Files
Leland KembleandGitHub af8dc77f81 Adds very large tripwires that trip mechs (#96895)
## About The Pull Request

<img width="194" height="63" alt="image"
src="https://github.com/user-attachments/assets/8b81ede5-3ac1-427c-b580-f75a4b05fae5"
/>

Tripwires, for mechs. A very sturdy cable hung over a space 2-5 tiles
wide, that mechs will trip over if they aren't paying attention.


https://github.com/user-attachments/assets/eb77ac44-73ad-43f7-aa48-3008aedc79e0

It also hurts people it falls on in pretty much the same way as a
vending machine. (it's using `fall_and_crush()`)


https://github.com/user-attachments/assets/97fccb33-d479-4170-8601-07a5b32fe740

To humans, the tripwire blocks movement unless you carefully step over
it(walk) or crawl under it. All beings suffer no issue walking across
the tripwire the long way.

The tripwire is created from wiring together two tripwire poles with a
tripwire cable item. The poles are created from iron rods and iron, the
cable is created from just cable. The wiring takes 3 seconds * how far
the posts are to complete, to a maximum of 12 seconds.

They take significantly increased (10x) damage from any sharp object,
and can be cut in a few seconds using wirecutters. They otherwise have a
`max_integrity` of 500, so a powerful demolition tool(like a mech's
fists or a drill) or a sharp object are fairly necessary to take them
down in a combat-reasonable amount of time.

The tripping has four effects - the damage to the being that the mech
fell on, the paralyzing of that being, damage to the mech pilot's head,
and the time it takes for the mech to get back up. All four of these
numbers are based off of the mech's max health divided by 5, or
`damage_value`.

`damage_value` is applied directly as damage to whoever it falls on.
That person is paralyzed for `damage_value` / 10 seconds.
The pilot of the falling mech recieves `damage_value` / 5 to their head,
with a significant wound bonus. Wear a helmet.
The mech itself recieves `damage_value` * 0.15 seconds of immobility(can
still attack, but can't rotate). (`damage_value` / 10 * 1.5)
What these numbers mean on common exosuits:
- odysseus - mob under hit for 24 damage & 2.4 seconds, pilot hit for ~5
damage, immobile for 3.6 seconds
- ripley - mob under hit for 40 damage & 4 seconds, pilot hit for 8
damage, immobile for 6 seconds
- gygax - mob under hit for 50 damage & 5 seconds, pilot hit for 10
damage, immobile for 7.5 seconds
- durand - mob under hit for 80 damage & 8 seconds, pilot hit for 16
damage, immobile for 12 seconds
- marauder - mob under hit for 100 damage & 10 seconds, pilot hit for 20
damage, immobile for 15 seconds

All of these stun times are large enough to where if there were multiple
people with good weapons around, they could most certainly kill you
before you stand back up. So, don't trip.

Finally, obviously, the clarke is immune. Who'd have thunk.

## Why It's Good For The Game

First, let's all thank the ideas guy. The ideas guy for today is
[dubblywumps](https://discord.com/channels/699727870846697493/705679956021215264/1523883105276919969).
Thank you, dubblywumps. Good idea.

So, what's the point of it? It's an effective area denial tool and
fortification specifically effective against mechs. You can set them up
in the hallways in a reasonable length of time, and it'll at the very
least slow a rampaging mech down, as while they're not strong enough to
handle more than a few punches from a mech, they're strong enough to
handle a few. Falling over as a mech is obviously extremely punishing,
so unless they can take a delay to destroy the wire(or a long delay to
destroy the wire at range), or get around it by tearing through
walls(also a delay), they have to abide by your preferred positioning.
No other tool exists that can truly force a delay in a mech's movement
besides actual walls & windows, which delay humans far more
significantly.

It is not remotely practical in a directly offensive sense, with even
the shortest 2-tile barricades taking too long to set up to do so before
you've been clobbered and perforated. It is practical in an indirectly
offensive sense, to force a fight to continue and prevent the worst part
about fighting a mech: when it walks away. This requires forethought or
teamwork, though. It is also practical in a defensive sense, in
combination with normal barricades to block projectiles you could
reasonably hold a mech off for a while or even potentially force a
stalemate using ranged weapons.

They're very, very cheap materials-wise, but the expense in their
creation is in the time to set them up in a good location. Because they
also slow down people, people won't be setting scores of them up in the
middle of the halls. And if they did, wirecutters take them down faster
than they can be put up.

You may be balking about the immobility time applied to the mech. The
numbers mean that in order to destroy the mech while it lays on the
ground, you must deal a flat 33 + armor damage per second for the
duration of the stun. This is an unreasonable task for one person with a
good crew melee weapon, a reasonable task for two, and an easy task for
three. I think that two people being able to beat you to death after you
failed to take heed of the not-at-all-hidden anti-mech weapon is
reasonable. This is also assuming you have no support team or second
mech, which would be perfectly capable of defending you during your
stun.

## Changelog
🆑

add: Added craftable anti-mech tripwires. A mech that foolishly steps on
one will fall down and be immobilized temporarily. Don't let it fall on
you!

/🆑
2026-07-16 22:07:35 +01:00

207 lines
7.2 KiB
Plaintext

/// Sets the direction of the mecha and all of its occcupents, required for FOV. Alternatively one could make a recursive contents registration and register topmost direction changes in the fov component
/obj/vehicle/sealed/mecha/setDir(newdir)
. = ..()
for(var/mob/living/occupant as anything in occupants)
occupant.setDir(newdir)
///Called when the mech moves
/obj/vehicle/sealed/mecha/proc/on_move()
SIGNAL_HANDLER
collect_ore()
play_stepsound()
///Collects ore when we move, if there is an orebox and it is functional
/obj/vehicle/sealed/mecha/proc/collect_ore()
if(isnull(ore_box) || !HAS_TRAIT(src, TRAIT_OREBOX_FUNCTIONAL))
return
for(var/obj/item/stack/ore/ore in range(1, src))
//we can reach it and it's in front of us? grab it!
if(ore.Adjacent(src) && ((get_dir(src, ore) & dir) || ore.loc == loc))
ore.forceMove(ore_box)
for(var/obj/item/boulder/boulder in range(1, src))
//As above, but for boulders
if(boulder.Adjacent(src) && ((get_dir(src, boulder) & dir) || boulder.loc == loc))
boulder.forceMove(ore_box)
///Plays the mech step sound effect. Split from movement procs so that other mechs (HONK) can override this one specific part.
/obj/vehicle/sealed/mecha/proc/play_stepsound()
if(mecha_flags & QUIET_STEPS)
return
// if we are on the second step of the diagonal movement, don't play step sound
if(src.moving_diagonally == SECOND_DIAG_STEP)
return
playsound(src, stepsound, 40, TRUE)
// Do whatever you do to mobs to these fuckers too
/obj/vehicle/sealed/mecha/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
. = ..()
if(.)
return TRUE
var/atom/movable/backup = get_spacemove_backup(movement_dir, continuous_move)
if(backup)
if(!istype(backup) || !movement_dir || backup.anchored || continuous_move) //get_spacemove_backup() already checks if a returned turf is solid, so we can just go
return TRUE
last_pushoff = world.time
if(backup.newtonian_move(dir2angle(REVERSE_DIR(movement_dir)), instant = TRUE))
backup.last_pushoff = world.time
step_silent = TRUE
if(return_drivers())
to_chat(occupants, "[icon2html(src, occupants)][span_info("The [src] push off [backup] to propel yourself.")]")
return TRUE
if(active_thrusters?.thrust(movement_dir))
step_silent = TRUE
return TRUE
return FALSE
///Called when the driver turns with the movement lock key
/obj/vehicle/sealed/mecha/proc/on_turn(mob/living/driver, direction)
SIGNAL_HANDLER
return COMSIG_IGNORE_MOVEMENT_LOCK
/obj/vehicle/sealed/mecha/relaymove(mob/living/user, direction)
. = TRUE
if(!canmove || !(user in return_drivers()))
return
if (!vehicle_move(direction))
return
SEND_SIGNAL(user, COMSIG_MOB_DROVE_MECH, src)
/obj/vehicle/sealed/mecha/vehicle_move(direction, forcerotate = FALSE)
if(!COOLDOWN_FINISHED(src, cooldown_vehicle_move))
return FALSE
COOLDOWN_START(src, cooldown_vehicle_move, movedelay)
if(completely_disabled)
return FALSE
if(!direction)
return FALSE
if(!can_move(direction))
return FALSE
if(ismovable(loc)) //Mech is inside an object, tell it we moved
var/atom/loc_atom = loc
return loc_atom.relaymove(src, direction)
if(!Process_Spacemove(direction))
return FALSE
var/olddir = dir
if(internal_damage & MECHA_INT_CONTROL_LOST)
direction = pick(GLOB.alldirs)
//only mechs with diagonal movement may move diagonally
if(!allow_diagonal_movement && ISDIAGONALDIR(direction))
return TRUE
var/keyheld = FALSE
if(strafe)
for(var/mob/driver as anything in return_drivers())
if(driver.client?.keys_held["Alt"])
keyheld = TRUE
break
//if we're not facing the way we're going rotate us
// if we're not strafing or if we are forced to rotate or if we are holding down the key
if(dir != direction && (!strafe || forcerotate || keyheld))
setDir(direction)
if(!(mecha_flags & QUIET_TURNS))
playsound(src, turnsound, 40, TRUE)
if(keyheld || !pivot_step) //If we pivot step, we don't return here so we don't just come to a stop
return TRUE
set_glide_size(DELAY_TO_GLIDE_SIZE(movedelay))
//Otherwise just walk normally
. = try_step_multiz(direction)
if(strafe)
setDir(olddir)
/// Check if anything is blocking our movement
/obj/vehicle/sealed/mecha/proc/can_move(direction)
if(toppled)
return FALSE
var/obj/machinery/portable_atmospherics/canister/internal_tank = get_internal_tank()
if(internal_tank?.connected_port)
if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Unable to move while connected to the air system port!")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
var/list/missing_parts = list()
if(isnull(cell))
missing_parts += "power cell"
if(isnull(capacitor))
missing_parts += "capacitor"
if(isnull(servo))
missing_parts += "servo"
if(length(missing_parts))
if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Missing [english_list(missing_parts)].")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
if((step_energy_drain != 0) && !use_energy(step_energy_drain))
if(TIMER_COOLDOWN_FINISHED(src, COOLDOWN_MECHA_MESSAGE))
to_chat(occupants, "[icon2html(src, occupants)][span_warning("Insufficient power to move!")]")
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MESSAGE, 2 SECONDS)
return FALSE
return TRUE
/obj/vehicle/sealed/mecha/Bump(atom/obstacle)
. = ..()
if(.) //mech was thrown/door/whatever
return
try_bumpsmash(obstacle)
/obj/vehicle/sealed/mecha/proc/try_bumpsmash(atom/obstacle)
// Whether or not we're on our mecha melee cooldown
var/on_cooldown = TIMER_COOLDOWN_RUNNING(src, COOLDOWN_MECHA_MELEE_ATTACK)
if(bumpsmash && !on_cooldown)
// Our pilot for this evening
var/list/mob/mobster = return_drivers()
if(obstacle.mech_melee_attack(src, mobster[1]))
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MELEE_ATTACK, melee_cooldown * 0.3)
if(!obstacle || obstacle.CanPass(src, get_dir(obstacle, src) || dir)) // The else is in case the obstacle is in the same turf.
step(src,dir)
if(isobj(obstacle))
var/obj/obj_obstacle = obstacle
if(!obj_obstacle.anchored && obj_obstacle.move_resist <= move_force)
step(obstacle, dir)
else if(ismob(obstacle))
var/mob/mob_obstacle = obstacle
if(mob_obstacle.move_resist <= move_force)
step(obstacle, dir)
//Following procs are camera static update related and are basically ripped off of code\modules\mob\living\silicon\silicon_movement.dm
//We only call a camera static update if we have successfully moved and have a camera installed
/obj/vehicle/sealed/mecha/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
if(!chassis_camera?.can_use())
return
// Delay's a bit faster then standard cameras to "avoid running out of the camera's fov" whatever that means
// An EMPd mecha with a lowered view_range on its camera can still sometimes run out into static before updating, however.
SScameras.camera_moved(chassis_camera, get_turf(old_loc), get_turf(chassis_camera), 0.5 SECONDS)
/obj/vehicle/sealed/mecha/proc/right_self(fallen_angle)
if(!toppled)
return
toppled = FALSE
var/matrix/to_turn = turn(transform, 360 - fallen_angle)
animate(src, transform = to_turn, 0.5 SECONDS)
playsound(src, 'sound/vehicles/mecha/nominal.ogg', 50)