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

347 lines
12 KiB
Plaintext

/obj/structure/tripwire
name = "tripwire post?"
desc = "This should not be here. Somebody should be told about this."
icon = 'icons/obj/tripwire.dmi'
icon_state = "tripwire_post"
anchored = TRUE
density = TRUE
abstract_type = /obj/structure/tripwire
/obj/structure/tripwire/post
name = "stanchion"
desc = "A sturdy post made to support one end of a large cable."
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 3.5)
/// The post that the other end of this post's tripwire, if any. Also serves as a check for if we have a tripwire.
var/obj/structure/tripwire/post/opposing_post
/// The wire or wires between us and the opposing post. Only one post in the pair will have this list filled.
var/list/obj/structure/tripwire/cable/tripwires = list()
/obj/structure/tripwire/post/Initialize(mapload)
. = ..()
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
/obj/structure/tripwire/post/Destroy(force)
clear_wire()
return ..()
/obj/structure/tripwire/post/proc/clear_wire()
if(opposing_post)
opposing_post.opposing_post = null
opposing_post.clear_wire()
opposing_post = null
for(var/tripwire in tripwires)
UnregisterSignal(tripwire, COMSIG_QDELETING)
qdel(tripwire)
tripwires = list()
update_appearance(UPDATE_ICON)
/obj/structure/tripwire/post/proc/wire_qdeleted(datum/source)
tripwires -= source
UnregisterSignal(source, COMSIG_QDELETING)
clear_wire()
/obj/structure/tripwire/post/proc/connect_to_post(obj/structure/tripwire/post/connected_post, new_direction)
dir = new_direction
opposing_post = connected_post
update_appearance(UPDATE_ICON)
/obj/structure/tripwire/post/update_appearance(updates)
. = ..()
icon_state = "tripwire_post[opposing_post ? "_tied" : ""]"
/obj/structure/tripwire/post/wirecutter_act(mob/living/user, obj/item/tool)
if(!opposing_post)
return NONE
visible_message(span_notice("[user] begins cutting through \the [src]'s tripwire..."), \
span_notice("You begin cutting through \the [src]'s tripwire..."), \
span_hear("You hear snipping."))
if(!tool.use_tool(src, user, 5 SECONDS))
return ITEM_INTERACT_BLOCKING
if(!opposing_post)
return ITEM_INTERACT_BLOCKING
visible_message(span_notice("[user] cuts away \the [src]'s tripwire."), \
span_notice("You finish cutting \the [src]'s tripwire."), \
span_hear("You hear a final snip."))
new /obj/item/stack/cable_coil(drop_location(), 20)
clear_wire()
return ITEM_INTERACT_SUCCESS
/obj/structure/tripwire/post/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(!istype(tool, /obj/item/tripwire_cable))
return NONE
if(!anchored || opposing_post)
return NONE
var/obj/item/tripwire_cable/irrelevant_cable = tool
var/obj/structure/tripwire/post/connecting_post = irrelevant_cable.connecting_post?.resolve()
var/are_we_tying = FALSE
if(!connecting_post || connecting_post.z != z)
are_we_tying = TRUE
var/distance_between = get_dist(src, connecting_post)
if(distance_between > 6)
are_we_tying = TRUE
if(are_we_tying)// if it's on a different z level, or further than would be reasonable, refuse to acknowledge the distance and mulligan.
to_chat(user, span_notice("You tie one end of \the [irrelevant_cable] around \the [src]."))
irrelevant_cable.connecting_post = WEAKREF(src)
return ITEM_INTERACT_SUCCESS
if(connecting_post == src)
return ITEM_INTERACT_BLOCKING
if(!connecting_post.anchored)
return ITEM_INTERACT_BLOCKING
if(distance_between > 4)
to_chat(user, span_notice("The post on the other end is too far away!"))
return ITEM_INTERACT_BLOCKING
if(distance_between == 0)
to_chat(user, span_notice("No point in connecting them, they're already together."))
return ITEM_INTERACT_BLOCKING
if(connecting_post.x != x && connecting_post.y != y)
to_chat(user, span_notice("The wire will be much less effective at an angle."))
return ITEM_INTERACT_BLOCKING
var/turf/end_point_turf = get_turf(connecting_post)
if(!do_after(user, distance_between * 3 SECONDS, src))
return ITEM_INTERACT_BLOCKING
if(!anchored || opposing_post || !connecting_post?.anchored || connecting_post.opposing_post || end_point_turf != get_turf(connecting_post))
return ITEM_INTERACT_BLOCKING
var/direction_to_extend = get_dir(src, connecting_post)
var/turf/iterating_turf = get_turf(src)
for(var/distance_extended in 1 to distance_between)
iterating_turf = get_step(iterating_turf, direction_to_extend)
if(iterating_turf == end_point_turf)
connect_to_post(connecting_post, direction_to_extend)
connecting_post.connect_to_post(src, REVERSE_DIR(direction_to_extend))
to_chat(user, span_notice("You successfully wire together the two posts."))
qdel(irrelevant_cable)
return ITEM_INTERACT_SUCCESS
if(iterating_turf.is_blocked_turf(TRUE))
clear_wire()
to_chat(user, span_notice("You can't feed the cable through solid objects."))
return ITEM_INTERACT_BLOCKING
var/obj/structure/tripwire/cable/new_wire = new(iterating_turf)
switch(distance_between)
// if(1) we'd have already dropped out at connecting the two posts together, and wouldn't get here
if(2)
new_wire.icon_state = "tripwire_1" // post - wire(1) - post
if(3)
new_wire.icon_state = "tripwire_2" // post - wire(2)(L) - wire(2)(R) - post
if(4)
new_wire.icon_state = "tripwire_[distance_extended == 2 ? "3" : "2"]" // post - wire(2)(L) - wire(3) - wire(2)(R) - post
new_wire.dir = (distance_extended > (distance_between / 2)) ? direction_to_extend : REVERSE_DIR(direction_to_extend) // Switch directions once we're halfway there
RegisterSignal(new_wire, COMSIG_QDELETING, PROC_REF(wire_qdeleted))
tripwires += new_wire
// HOW DID WE GET HERE WE SHOULD NEVER GET HERE BURN IT ALL
clear_wire()
return ITEM_INTERACT_BLOCKING
/obj/structure/tripwire/post/wrench_act(mob/living/user, obj/item/tool)
if(opposing_post)
balloon_alert(user, "unwire first!")
return ITEM_INTERACT_BLOCKING
if(!tool.use_tool(src, user, 0))
return ITEM_INTERACT_BLOCKING
balloon_alert(user, "[anchored ? "un" : ""]anchored")
set_anchored(!anchored)
return ITEM_INTERACT_SUCCESS
/obj/structure/tripwire/post/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
clear_wire()
return ..()
/obj/structure/tripwire/post/CanAllowThrough(atom/movable/mover, border_dir)
if(!opposing_post)
return TRUE
return ..()
/obj/structure/tripwire/post/on_entered(datum/source, atom/movable/entered)
if(!opposing_post)
return
return ..()
/obj/structure/tripwire/cable
name = "heavy cable"
desc = "A very thick cable made to withstand great force."
icon_state = "tripwire_1"
/obj/structure/tripwire/cable/Initialize(mapload)
. = ..()
RegisterSignal(get_turf(src), COMSIG_ATOM_ENTERED, PROC_REF(on_entered)) // No need for connect_loc, we won't be moving.
/obj/structure/tripwire/cable/attackby(obj/item/attacking_item, mob/user, list/modifiers, list/attack_modifiers)
if(attacking_item.sharpness)
MODIFY_ATTACK_FORCE_MULTIPLIER(attack_modifiers, 10)
return ..()
/obj/structure/tripwire/cable/wirecutter_act(mob/living/user, obj/item/tool)
visible_message(span_notice("[user] begins cutting through \the [src]..."), \
span_notice("You begin cutting through \the [src]..."), \
span_hear("You hear snipping."))
if(!tool.use_tool(src, user, 5 SECONDS))
return ITEM_INTERACT_BLOCKING
visible_message(span_notice("[user] cuts away \the [src]."), \
span_notice("You finish cutting \the [src]."), \
span_hear("You hear a final snip."))
new /obj/item/stack/cable_coil(drop_location(), 20)
qdel(src)
return ITEM_INTERACT_SUCCESS
/obj/structure/tripwire/cable/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
if(!QDELING(src))
qdel(src)
/obj/structure/tripwire/CanAllowThrough(atom/movable/mover, border_dir)
. = ..()
if(.)
return .
if(isprojectile(mover))
return TRUE
if(mover.dir == dir || mover.dir == REVERSE_DIR(dir))
return TRUE
if(!mover.has_gravity())
return TRUE
if(mover.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
return TRUE
if(iscarbon(mover))
var/mob/living/carbon/moving_carbon = mover
if(moving_carbon.body_position == LYING_DOWN) // Crawl under it. Bonus of allowing dragging dying people.
return TRUE
if(moving_carbon.move_intent == MOVE_INTENT_WALK)
return TRUE
if(ismecha(mover))
if(istype(mover, /obj/vehicle/sealed/mecha/clarke)) // treads
return FALSE
return TRUE
return FALSE
/obj/structure/tripwire/proc/on_entered(datum/source, atom/movable/entered)
if(!ismecha(entered))
return
var/obj/vehicle/sealed/mecha/falling_down = entered
if(falling_down.toppled) // did you trip directly into a tripwire? We don't want this to chain, however amusing the image might be.
return
if(astype(falling_down, /obj/vehicle/sealed/mecha/phazon)?.phasing)
return
if(falling_down.dir == dir || falling_down.dir == REVERSE_DIR(dir))
return
var/damage_value = falling_down.max_integrity / 5
var/falling_angle
if(falling_down.dir == WEST)
falling_angle = 270
else if(falling_down.dir == EAST)
falling_angle = 90
else
falling_angle = pick(90, 270)
// For disambiguation, damage_value is the mech's health / 5. damage_value is the base for all the rest of the numbers.
// damage_value is applied directly as damage to whoever it falls on. That person is also paralyzed for damage_value / 10 seconds.
// The pilot of the falling mech receives damage_value / 5 as a head trauma.
// The mech itself receives damage_value * 0.15 seconds of immobility(can still attack, but can't rotate). (damage_value / 10 * 1.5)
falling_down.toppled = TRUE
falling_down.forceMove(get_turf(src))
var/tripped = falling_down.fall_and_crush(get_step(falling_down, falling_down.dir), damage_value, 15, paralyze_time = damage_value, rotation = falling_angle)
addtimer(CALLBACK(falling_down, TYPE_PROC_REF(/obj/vehicle/sealed/mecha, right_self), falling_angle), damage_value * 1.5)
if (!(tripped & SUCCESSFULLY_FELL_OVER))
falling_down.toppled = FALSE
return // Don't know how it'd be avoided, but if you beat it, you beat it
var/drivers = falling_down.return_drivers()
visible_message(span_danger("[falling_down] topples over \the [src]!"), \
blind_message = span_danger("You hear a deafening CRASH!"), \
ignored_mobs = drivers)
for(var/mob/living/driver as anything in drivers) // can any mechs have two drivers? No. Could they? yes.
var/damage = driver.apply_damage(damage_value / 3, BRUTE, BODY_ZONE_HEAD, driver.run_armor_check(BODY_ZONE_HEAD, MELEE), wound_bonus = 30, wound_clothing = FALSE)
var/falling_string
switch(damage)
if(0)
falling_string = "You rapidly fall forward but only lightly hit [falling_down]'s controls. "
if(0 to 10)
falling_string = "You rapidly fall forward and hit your head on [falling_down]'s controls! "
if(10 to INFINITY)
falling_string = "You rapidly fall forward and smash your head against [falling_down]'s controls! "
falling_string += "When you recover, you [damage_value > 40 ? "slowly" : "quickly"] begin righting [falling_down]."
to_chat(driver, span_danger(falling_string))
/obj/item/tripwire_cable
name = "sturdy cable"
desc = "A coil of extremely thick cable that can withstand great force."
icon = 'icons/obj/tripwire.dmi'
icon_state = "tripwire_cable"
lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
inhand_icon_state = "coil_yellow"
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 3, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 3)
/// The post we're trying to connect at the moment, if any.
var/datum/weakref/connecting_post
/obj/item/tripwire_cable/attack_self(mob/user, modifiers)
if(!connecting_post.resolve())
return
to_chat(user, span_notice("You recoil the length of cable until it's all back together again."))
connecting_post = null
/obj/item/tripwire_cable/wirecutter_act(mob/living/user, obj/item/tool)
to_chat(user, span_notice("You cut off the cable's excess bulk."))
new /obj/item/stack/cable_coil(drop_location(), 15)
qdel(src)
return ITEM_INTERACT_SUCCESS
/datum/crafting_recipe/tripwire_cable
name = "Tripwire Cable"
result = /obj/item/tripwire_cable
reqs = list(
/obj/item/stack/cable_coil = 30,
)
tool_behaviors = list(TOOL_WIRECUTTER)
time = 10 SECONDS
category = CAT_MISC