Files
necromanceranneandGitHub ac6c47f601 Several file changes just to make high frequency blade null rods block mechs, mech melee blocking finally implemented (#94089)
## About The Pull Request

High frequency blades, both null rod variant and admeme variant, can
block mech melee attacks. For the former, that is the only thing it can
block. Great if you're fighting a lot of mechs as a chaplain I guess.
And you're also really committed to the bit.

Weapons that were vibro subtypes are now claymore subtypes. The only
difference here is that they now have 30% block instead of 35% AP.

Null rod high frequency blades are now two-handed. This mostly
determines force changes (10 unwielded, 18 wielded), and mech attack
block chances.

Mech melee attacks respect block...assuming the blocking source can even
block the attack in the first place. Currently, the few sources that can
block/avoid mech attacks are;
- high frequency blades
- energy katanas
- Those staff nullrods (its a holy force field, iunno)
- wizard modsuit shield (and it will eat literally every single charge
it has to do that)
- Sleeping Carp evasion (and only at half the possible chance)
- CQC defense mode (themed as dodging the attack, and also halved)
- probably something I overlooked when I implemented OVERWHELMING_ATTACK
originally and have overlooked here as well.
 
Just think 'could this help avoid being hit by a road roller being
dropped on you' and go with your gut as to whether or not it is
appropriate if you spot something in the wild that seemingly can block a
mech.

## Why It's Good For The Game

> High Frequency Blade

I wanted to elevate the meme and that is as deep as this goes. Also I
wanted to make this two-handed, much like the true version.
The only thing it is lacking is the cool attack functionality, but I'll
be damned if I can get that to work in a sensible fashion for a crew
weapon...

> Vibro subtype

A lot less of these than I thought. And most of them seemed more
appropriate as claymore types anyway.

> Mech Block

I seeded this previously by making mech clamps respect block. I've now
come back to fully implement it in order to facilitate MGR memes.

For the most part, mechs should still enjoy being largely unblocked by
most sources, even from things like operative suit shields, and
certainly not from actual shields (which I've stripped the ability to
block the attack from entirely) and melee weapons.

The sources that can block them do so poorly. Or, in the case of the
high frequency blade, very well and that's pretty much the only thing it
IS good at blocking.

## Changelog
🆑
balance: High frequency blades (both null rod and admin versions) are
able to block melee attacks from mechs.
balance: High frequency blade null rods are now two-handed weapons.
balance: Mech melee attacks can be blocked or avoided by a few rare
sources.
/🆑
2025-12-02 01:13:58 -05:00

410 lines
16 KiB
Plaintext

//Hydraulic clamp, Kill clamp, Extinguisher, RCD, Cable layer.
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp
name = "hydraulic clamp"
desc = "Equipment for engineering exosuits. Lifts objects and loads them into cargo."
icon_state = "mecha_clamp"
equip_cooldown = 15
energy_drain = 0.01 * STANDARD_CELL_CHARGE
tool_behaviour = TOOL_RETRACTOR
range = MECHA_MELEE
toolspeed = 0.8
harmful = TRUE
mech_flags = EXOSUIT_MODULE_RIPLEY
///Bool for whether we beat the hell out of things we punch (and tear off their arms)
var/killer_clamp = FALSE
///How much base damage this clamp does
var/clamp_damage = 20
///Audio for using the hydraulic clamp
var/clampsound = 'sound/vehicles/mecha/hydraulic.ogg'
///Chassis but typed for the cargo_hold var
var/obj/vehicle/sealed/mecha/ripley/workmech
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp/attach(obj/vehicle/sealed/mecha/new_mecha)
. = ..()
workmech = chassis
ADD_TRAIT(chassis, TRAIT_OREBOX_FUNCTIONAL, TRAIT_MECH_EQUIPMENT(type))
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp/detach(atom/moveto)
REMOVE_TRAIT(chassis, TRAIT_OREBOX_FUNCTIONAL, TRAIT_MECH_EQUIPMENT(type))
workmech = null
return ..()
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp/use_tool(atom/target, mob/living/user, delay, amount, volume, datum/callback/extra_checks)
return do_after_mecha(target, user, delay)
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp/do_after_checks(atom/target)
// Gotta be close to the target
if(!loc.Adjacent(target))
return FALSE
return ..()
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp/action(mob/living/source, atom/target, list/modifiers)
if(!action_checks(target))
return
if(!workmech.cargo_hold)
CRASH("Mech [chassis] has a clamp device, but no internal storage. This should be impossible.")
if(ismecha(target))
var/obj/vehicle/sealed/mecha/M = target
var/have_ammo
for(var/obj/item/mecha_ammo/box in workmech.cargo_hold.contents)
if(istype(box, /obj/item/mecha_ammo) && box.rounds)
have_ammo = TRUE
if(M.ammo_resupply(box, source, TRUE))
return ..()
if(have_ammo)
to_chat(source, "No further supplies can be provided to [M].")
else
to_chat(source, "No providable supplies found in cargo hold")
return
if(istype(target, /obj/machinery/door/firedoor) || istype(target, /obj/machinery/door/airlock))
var/obj/machinery/door/target_door = target
playsound(chassis, clampsound, 50, FALSE, -6)
target_door.try_to_crowbar(src, source, TRUE)
return ..()
if(isobj(target))
var/obj/clamptarget = target
if(clamptarget.anchored)
to_chat(source, "[icon2html(src, source)][span_warning("[target] is firmly secured!")]")
return
if(workmech.cargo_hold.contents.len >= workmech.cargo_hold.cargo_capacity)
to_chat(source, "[icon2html(src, source)][span_warning("Not enough room in cargo compartment!")]")
return
playsound(chassis, clampsound, 50, FALSE, -6)
chassis.visible_message(span_notice("[chassis] lifts [target] and starts to load it into cargo compartment."))
clamptarget.set_anchored(TRUE)
if(!do_after_cooldown(target, source, flags = MECH_DO_AFTER_DIR_CHANGE_FLAG|MECH_DO_AFTER_ADJACENCY_FLAG))
clamptarget.set_anchored(FALSE)
return
clamptarget.set_anchored(FALSE)
clamptarget.forceMove(workmech.cargo_hold)
if(!chassis.ore_box && istype(clamptarget, /obj/structure/ore_box))
chassis.ore_box = clamptarget
to_chat(source, "[icon2html(src, source)][span_notice("[target] successfully loaded.")]")
log_message("Loaded [clamptarget]. Cargo compartment capacity: [workmech.cargo_hold.cargo_capacity - workmech.cargo_hold.contents.len]", LOG_MECHA)
return ..()
if(!isliving(target))
return ..()
var/mob/living/victim = target
if(victim.stat == DEAD)
return ..()
if(!source.combat_mode)
step_away(victim, chassis)
if(killer_clamp)
target.visible_message(span_danger("[chassis] tosses [target] like a piece of paper!"), \
span_userdanger("[chassis] tosses you like a piece of paper!"))
else
to_chat(source, "[icon2html(src, source)][span_notice("You push [target] out of the way.")]")
chassis.visible_message(span_notice("[chassis] pushes [target] out of the way."), \
span_notice("[chassis] pushes you aside."))
return ..()
if(victim.check_block(chassis, clamp_damage, name, attack_type = OVERWHELMING_ATTACK))
return ..()
if(iscarbon(victim) && killer_clamp)//meme clamp here
var/mob/living/carbon/carbon_victim = target
var/torn_off = FALSE
var/obj/item/bodypart/affected = carbon_victim.get_bodypart(BODY_ZONE_L_ARM)
if(affected != null)
affected.dismember(damtype)
torn_off = TRUE
affected = carbon_victim.get_bodypart(BODY_ZONE_R_ARM)
if(affected != null)
affected.dismember(damtype)
torn_off = TRUE
if(torn_off)
playsound(src, get_dismember_sound(), 80, TRUE)
carbon_victim.visible_message(span_danger("[chassis] rips [carbon_victim]'s arms off!"), \
span_userdanger("[chassis] rips your arms off!"))
log_combat(source, carbon_victim, "removed both arms with a real clamp,", "[name]", "(COMBAT MODE: [uppertext(source.combat_mode)] (DAMTYPE: [uppertext(damtype)])")
return ..()
var/armor_check = clamp(victim.run_armor_check(null, MELEE) / 3, 0, 100) //our target only benefits from a third of their armor. Because it's a huge ass clamp
victim.visible_message(span_danger("[chassis] squeezes [victim]!"), \
span_userdanger("[chassis] squeezes you!"),\
span_hear("You hear something crack."))
log_combat(source, victim, "attacked", "[name]", "(Combat mode: [source.combat_mode ? "On" : "Off"]) (DAMTYPE: [uppertext(damtype)])")
var/final_damage = isalien(victim) ? clamp_damage * 3 : clamp_damage
chassis.do_attack_animation(victim)
playsound(chassis, clampsound, 30, FALSE, -6)
victim.apply_damage(final_damage, BRUTE, blocked = armor_check, spread_damage = TRUE)
return ..()
//This is pretty much just for the death-ripley
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp/kill
name = "\improper KILL CLAMP"
desc = "They won't know what clamped them! This time for real!"
killer_clamp = TRUE
/obj/item/mecha_parts/mecha_equipment/hydraulic_clamp/kill/fake//harmless fake for pranks
desc = "They won't know what clamped them!"
energy_drain = 0
clamp_damage = 0
killer_clamp = FALSE
/obj/item/mecha_parts/mecha_equipment/extinguisher
name = "exosuit extinguisher"
desc = "Equipment for engineering exosuits. A rapid-firing high capacity fire extinguisher."
icon_state = "mecha_exting"
equip_cooldown = 5
energy_drain = 0
equipment_slot = MECHA_UTILITY
range = MECHA_MELEE|MECHA_RANGED
mech_flags = ALL
can_be_triggered = TRUE
action_type = /datum/action/vehicle/sealed/mecha/equipment/extinguisher_action
///Minimum amount of reagent needed to activate.
var/required_amount = 80
/obj/item/mecha_parts/mecha_equipment/extinguisher/Initialize(mapload)
. = ..()
create_reagents(400)
reagents.add_reagent(/datum/reagent/water, 400)
/obj/item/mecha_parts/mecha_equipment/extinguisher/proc/spray_extinguisher(mob/user)
if(reagents.total_volume < required_amount)
return
for(var/turf/targetturf in RANGE_TURFS(1, chassis))
var/obj/effect/particle_effect/water/extinguisher/water = new /obj/effect/particle_effect/water/extinguisher(targetturf)
var/datum/reagents/water_reagents = new /datum/reagents(required_amount/8) //required_amount/8, because the water usage is split between eight sprays. As of this comment, required_amount/8 = 10u each.
water.reagents = water_reagents
water_reagents.my_atom = water
reagents.trans_to(water, required_amount / 8)
water.move_at(get_step(chassis, get_dir(targetturf, chassis)), 2, 4) //Target is the tile opposite of the mech as the starting turf.
playsound(chassis, 'sound/effects/extinguish.ogg', 75, TRUE, -3)
/**
* Handles attempted refills of the extinguisher.
*
* The mech can only refill an extinguisher that is in front of it.
* Only water tank objects can be used.
*/
/obj/item/mecha_parts/mecha_equipment/extinguisher/proc/attempt_refill(mob/user)
if(reagents.maximum_volume == reagents.total_volume)
return
var/turf/in_front = get_step(chassis, chassis.dir)
var/obj/structure/reagent_dispensers/watertank/refill_source = locate(/obj/structure/reagent_dispensers/watertank) in in_front
if(!refill_source)
to_chat(user, span_notice("Refill failed. No compatible tank found."))
return
if(!refill_source.reagents?.total_volume)
to_chat(user, span_notice("Refill failed. Source tank empty."))
return
refill_source.reagents.trans_to(src, reagents.maximum_volume)
playsound(chassis, 'sound/effects/refill.ogg', 50, TRUE, -6)
/obj/item/mecha_parts/mecha_equipment/extinguisher/get_snowflake_data()
return list(
"snowflake_id" = MECHA_SNOWFLAKE_ID_EXTINGUISHER,
"reagents" = reagents.total_volume,
"total_reagents" = reagents.maximum_volume,
"reagents_required" = required_amount,
)
/obj/item/mecha_parts/mecha_equipment/extinguisher/handle_ui_act(action, list/params)
switch(action)
if("activate")
spray_extinguisher(usr)
return TRUE
if("refill")
attempt_refill(usr)
return TRUE
///Maximum range the RCD can construct at.
#define RCD_RANGE 3
/obj/item/mecha_parts/mecha_equipment/rcd
name = "mounted RCD"
desc = "An exosuit-mounted Rapid Construction Device."
icon_state = "mecha_rcd"
equip_cooldown = 0 // internal RCD already handles it
energy_drain = 0 // internal RCD handles power consumption based on matter use
range = MECHA_MELEE | MECHA_RANGED
item_flags = NO_MAT_REDEMPTION
///The location the mech was when it began using the rcd
var/atom/initial_location = FALSE
///Whether or not to deconstruct instead.
var/deconstruct_active = FALSE
///The internal RCD item used by this equipment.
var/obj/item/construction/rcd/exosuit/internal_rcd
/obj/item/mecha_parts/mecha_equipment/rcd/Initialize(mapload)
. = ..()
internal_rcd = new(src)
/obj/item/mecha_parts/mecha_equipment/rcd/Destroy()
initial_location = null
QDEL_NULL(internal_rcd)
return ..()
/obj/item/mecha_parts/mecha_equipment/rcd/get_snowflake_data()
return list(
"snowflake_id" = MECHA_SNOWFLAKE_ID_RCD,
"scan_ready" = COOLDOWN_FINISHED(internal_rcd, destructive_scan_cooldown),
"deconstructing" = deconstruct_active,
"mode" = internal_rcd.design_title,
)
/// Set the RCD's owner when attaching and detaching it
/obj/item/mecha_parts/mecha_equipment/rcd/attach(obj/vehicle/sealed/mecha/new_mecha, attach_right)
internal_rcd.owner = new_mecha
return ..()
/obj/item/mecha_parts/mecha_equipment/rcd/detach(atom/moveto)
internal_rcd.owner = null
return ..()
/obj/item/mecha_parts/mecha_equipment/rcd/handle_ui_act(action, list/params)
switch(action)
if("rcd_scan")
if(!COOLDOWN_FINISHED(internal_rcd, destructive_scan_cooldown))
return FALSE
rcd_scan(internal_rcd)
COOLDOWN_START(internal_rcd, destructive_scan_cooldown, RCD_DESTRUCTIVE_SCAN_COOLDOWN)
return TRUE
if("toggle_deconstruct")
deconstruct_active = !deconstruct_active
return TRUE
if("change_mode")
for(var/mob/driver as anything in chassis.return_drivers())
internal_rcd.ui_interact(driver)
return TRUE
/obj/item/mecha_parts/mecha_equipment/rcd/do_after_checks(atom/target)
// Checks if mech moved during operation
if(chassis.loc != initial_location)
return FALSE
// Cancel build if design changes
if(!deconstruct_active && internal_rcd.blueprint_changed)
return FALSE
return ..()
/obj/item/mecha_parts/mecha_equipment/rcd/action(mob/source, atom/target, list/modifiers)
if(!action_checks(target))
return
// No meson action!
if (!(target in view(RCD_RANGE, get_turf(chassis))))
return
if(get_dist(chassis, target) > RCD_RANGE)
balloon_alert(source, "out of range!")
return
initial_location = chassis.loc
..() // do this now because the do_after can take a while
var/construction_mode = internal_rcd.mode
if(deconstruct_active) // deconstruct isn't in the RCD menu so switch it to deconstruct mode and set it back when it's done
internal_rcd.mode = RCD_DECONSTRUCT
internal_rcd.rcd_create(target, source)
internal_rcd.mode = construction_mode
return TRUE
/obj/item/mecha_parts/mecha_equipment/rcd/interact_with_atom(obj/item/attacking_item, mob/living/user, list/modifiers)
. = NONE
if(istype(attacking_item, /obj/item/rcd_upgrade))
internal_rcd.install_upgrade(attacking_item, user)
return ITEM_INTERACT_SUCCESS
#undef RCD_RANGE
//Dunno where else to put this so shrug
/obj/item/mecha_parts/mecha_equipment/ripleyupgrade
name = "Ripley MK-II Conversion Kit"
desc = "A pressurized canopy attachment kit for an Autonomous Power Loader Unit \"Ripley\" MK-I exosuit, to convert it to the slower, but space-worthy MK-II design. This kit cannot be removed, once applied."
icon_state = "ripleyupgrade"
mech_flags = EXOSUIT_MODULE_RIPLEY
var/result = /obj/vehicle/sealed/mecha/ripley/mk2
/obj/item/mecha_parts/mecha_equipment/ripleyupgrade/can_attach(obj/vehicle/sealed/mecha/ripley/mecha, attach_right = FALSE, mob/user)
if(mecha.type != /obj/vehicle/sealed/mecha/ripley)
to_chat(user, span_warning("This conversion kit can only be applied to APLU MK-I models."))
return FALSE
var/obj/vehicle/sealed/mecha/ripley/workmech = mecha
if(LAZYLEN(workmech.cargo_hold))
to_chat(user, span_warning("[mecha]'s cargo hold must be empty before this conversion kit can be applied."))
return FALSE
if(!(mecha.mecha_flags & PANEL_OPEN)) //non-removable upgrade, so lets make sure the pilot or owner has their say.
to_chat(user, span_warning("[mecha] panel must be open in order to allow this conversion kit."))
return FALSE
if(LAZYLEN(mecha.occupants)) //We're actually making a new mech and swapping things over, it might get weird if players are involved
to_chat(user, span_warning("[mecha] must be unoccupied before this conversion kit can be applied."))
return FALSE
if(!mecha.cell) //Turns out things break if the cell is missing
to_chat(user, span_warning("The conversion process requires a cell installed."))
return FALSE
return TRUE
/obj/item/mecha_parts/mecha_equipment/ripleyupgrade/attach(obj/vehicle/sealed/mecha/markone, attach_right = FALSE)
var/obj/vehicle/sealed/mecha/newmech = new result(get_turf(markone),1)
if(!newmech)
return
QDEL_NULL(newmech.cell)
if (markone.cell)
newmech.cell = markone.cell
markone.cell.forceMove(newmech)
markone.cell = null
QDEL_NULL(newmech.scanmod)
if (markone.scanmod)
newmech.scanmod = markone.scanmod
markone.scanmod.forceMove(newmech)
markone.scanmod = null
QDEL_NULL(newmech.capacitor)
if (markone.capacitor)
newmech.capacitor = markone.capacitor
markone.capacitor.forceMove(newmech)
markone.capacitor = null
QDEL_NULL(newmech.servo)
if (markone.servo)
newmech.servo = markone.servo
markone.servo.forceMove(newmech)
markone.servo = null
newmech.update_part_values()
for(var/obj/item/mecha_parts/mecha_equipment/equipment in markone.flat_equipment) //Move the equipment over...
if(istype(equipment, /obj/item/mecha_parts/mecha_equipment/ejector))
continue //the new mech already has one.
var/righthandgun = markone.equip_by_category[MECHA_R_ARM] == equipment
equipment.detach(newmech)
equipment.attach(newmech, righthandgun)
newmech.dna_lock = markone.dna_lock
newmech.mecha_flags |= markone.mecha_flags & ~initial(markone.mecha_flags) // transfer any non-inherent flags like PANEL_OPEN and LIGHTS_ON
newmech.set_light_on(newmech.mecha_flags & LIGHTS_ON) // in case the lights were on
newmech.strafe = markone.strafe
//Integ set to the same percentage integ as the old mecha, rounded to be whole number
newmech.update_integrity(round((markone.get_integrity() / markone.max_integrity) * newmech.get_integrity()))
if(markone.name != initial(markone.name))
newmech.name = markone.name
markone.wreckage = FALSE
if(HAS_TRAIT(markone, TRAIT_MECHA_CREATED_NORMALLY))
ADD_TRAIT(newmech, TRAIT_MECHA_CREATED_NORMALLY, REF(newmech))
qdel(markone)
playsound(get_turf(newmech),'sound/items/tools/ratchet.ogg',50,TRUE)
/obj/item/mecha_parts/mecha_equipment/ripleyupgrade/paddy
name = "Paddy Conversion Kit"
desc = "A hardpoint modification kit for an Autonomous Power Loader Unit \"Ripley\" MK-I exosuit, to convert it to the Paddy lightweight security design. This kit cannot be removed, once applied."
icon_state = "paddyupgrade"
mech_flags = EXOSUIT_MODULE_RIPLEY
result = /obj/vehicle/sealed/mecha/ripley/paddy
/obj/item/mecha_parts/mecha_equipment/ripleyupgrade/paddy/can_attach(obj/vehicle/sealed/mecha/ripley/mecha, attach_right = FALSE, mob/user)
if(mecha.equip_by_category[MECHA_L_ARM] || mecha.equip_by_category[MECHA_R_ARM]) //Paddys can't use RIPLEY-type equipment
to_chat(user, span_warning("This kit cannot be applied with hardpoint equipment attached."))
return FALSE
return ..()