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

999 lines
38 KiB
Plaintext

/***************** WELCOME TO MECHA.DM, ENJOY YOUR STAY *****************/
/**
* Mechs are now (finally) vehicles, this means you can make them multicrew
* They can also grant select ability buttons based on occupant bitflags
*
* Movement is handled through vehicle_move() which is called by relaymove
* Clicking is done by way of signals registering to the entering mob
* NOTE: MMIS are NOT mobs but instead contain a brain that is, so you need special checks
* AI also has special checks becaus it gets in and out of the mech differently
* Always call remove_occupant(mob) when leaving the mech so the mob is removed properly
*
* For multi-crew, you need to set how the occupants receive ability bitflags corresponding to their status on the vehicle(i.e: driver, gunner etc)
* Abilities can then be set to only apply for certain bitflags and are assigned as such automatically
*
* Clicks are wither translated into mech_melee_attack (see mech_melee_attack.dm)
* Or are used to call action() on equipped gear
* Cooldown for gear is on the mech because exploits
* Cooldown for melee is on mech_melee_attack also because exploits
*/
/obj/vehicle/sealed/mecha
name = "exosuit"
desc = "Exosuit"
icon = 'icons/mob/rideables/mecha.dmi'
resistance_flags = FIRE_PROOF | ACID_PROOF
max_integrity = 300
armor_type = /datum/armor/sealed_mecha
force = 5
movedelay = 1 SECONDS
move_force = MOVE_FORCE_VERY_STRONG
move_resist = MOVE_FORCE_EXTREMELY_STRONG
light_system = OVERLAY_LIGHT_DIRECTIONAL
light_on = FALSE
light_range = 6
generic_canpass = FALSE
hud_possible = list(DIAG_STAT_HUD, DIAG_BATT_HUD, DIAG_MECH_HUD, DIAG_TRACK_HUD, DIAG_CAMERA_HUD)
mouse_pointer = 'icons/effects/mouse_pointers/mecha_mouse.dmi'
/// Significantly heavier than humans
inertia_force_weight = 5
/// How much energy the mech will consume each time it moves. this is the current active energy consumed
var/step_energy_drain = 0.008 * STANDARD_CELL_CHARGE
/// How much energy we drain each time we mechpunch someone
var/melee_energy_drain = 0.015 * STANDARD_CELL_CHARGE
/// Power we use to have the lights on
var/light_power_drain = 0.002 * STANDARD_CELL_RATE
/// Modifiers for directional damage reduction
var/list/facing_modifiers = list(MECHA_FRONT_ARMOUR = 0.5, MECHA_SIDE_ARMOUR = 1, MECHA_BACK_ARMOUR = 1.5)
/// If we cant use our equipment(such as due to EMP)
var/equipment_disabled = FALSE
/// Keeps track of the mech's cell
var/obj/item/stock_parts/power_store/cell
/// Keeps track of the mech's scanning module
var/obj/item/stock_parts/scanning_module/scanmod
/// Keeps track of the mech's capacitor
var/obj/item/stock_parts/capacitor/capacitor
/// Keeps track of the mech's servo motor
var/obj/item/stock_parts/servo/servo
/// Contains flags for the mecha
var/mecha_flags = CAN_STRAFE | IS_ENCLOSED | HAS_LIGHTS | MMI_COMPATIBLE | BEACON_TRACKABLE | AI_COMPATIBLE | BEACON_CONTROLLABLE
/// Spark effects are handled by this datum
var/datum/effect_system/basic/spark_spread/spark_system
/// How powerful our lights are
var/lights_power = 6
/// Just stop the mech from doing anything
var/completely_disabled = FALSE
/// Whether this mech is allowed to move diagonally
var/allow_diagonal_movement = TRUE
/// Whether this mech moves into a direct as soon as it goes to move. Basically, turn and step in the same key press.
var/pivot_step = FALSE
/// Whether or not the mech destroys walls by running into it.
var/bumpsmash = FALSE
// Atmos stuff
/// Whether the cabin exchanges gases with the environment
var/cabin_sealed = FALSE
/// Internal air mix datum
var/datum/gas_mixture/cabin_air
/// Volume of the cabin
var/cabin_volume = TANK_STANDARD_VOLUME * 3
/// List of installed remote tracking beacons, including AI control beacons
var/list/trackers = list()
/// Camera installed into the mech
var/obj/machinery/camera/exosuit/chassis_camera
var/max_temperature = 25000
/// Bitflags for internal damage
var/internal_damage = NONE
/// % chance for internal damage to occur
var/internal_damage_probability = 20
/// List of possibly dealt internal damage for this mech type
var/possible_int_damage = MECHA_INT_FIRE|MECHA_INT_TEMP_CONTROL|MECHA_CABIN_AIR_BREACH|MECHA_INT_CONTROL_LOST|MECHA_INT_SHORT_CIRCUIT
/// Damage threshold above which we take component damage
var/component_damage_threshold = 10
/// Stores the DNA enzymes of a carbon so tht only they can access the mech
var/dna_lock
/// A list of all granted accesses
var/list/accesses = list()
/// If the mech should require ALL or only ONE of the listed accesses
var/one_access = TRUE
/// Typepath for the wreckage it spawns when destroyed
var/wreckage
/// Single flag for the type of this mech, determines what kind of equipment can be attached to it
var/mech_type
/// Assoc list: key-typepathlist before init, key-equipmentlist after
var/list/equip_by_category = list(
MECHA_L_ARM = null,
MECHA_R_ARM = null,
MECHA_UTILITY = list(),
MECHA_POWER = list(),
MECHA_ARMOR = list(),
)
/// Assoc list: max equips for modules key-count
var/list/max_equip_by_category = list(
MECHA_L_ARM = 1,
MECHA_R_ARM = 1,
MECHA_UTILITY = 2,
MECHA_POWER = 1,
MECHA_ARMOR = 0,
)
/// Flat equipment for iteration
var/list/flat_equipment
/// Internal ore box for mining mechs, or one picked up by a hydraulic clamp
var/obj/structure/ore_box/ore_box
///Whether our steps are silent due to no gravity
var/step_silent = FALSE
/// Sound played when the mech moves
var/stepsound = 'sound/vehicles/mecha/mechstep.ogg'
/// Sound played when the mech walks
var/turnsound = 'sound/vehicles/mecha/mechturn.ogg'
/// Sounds for types of melee attack
var/brute_attack_sound = 'sound/items/weapons/punch4.ogg'
var/burn_attack_sound = 'sound/items/tools/welder.ogg'
var/tox_attack_sound = 'sound/effects/spray2.ogg'
/// Sound on wall destroying
var/destroy_wall_sound = 'sound/effects/meteorimpact.ogg'
/// Melee attack verb
var/list/attack_verbs = list("hit", "hits", "hitting")
/// Cooldown duration between melee punches
var/melee_cooldown = CLICK_CD_SLOW
/// Time taken to leave the mech
var/exit_delay = 2 SECONDS
/// Time you get slept for if you get forcible ejected by the mech exploding
var/destruction_sleep_duration = 2 SECONDS
/// In case theres a different iconstate for AI/MMI pilot(currently only used for ripley)
var/silicon_icon_state = null
/// Currently ejecting, and unable to do things
var/is_currently_ejecting = FALSE
/// Safety for weapons. Won't fire if enabled, and toggled by middle click.
var/weapons_safety = FALSE
/// Don't play standard sound when set safety if TRUE.
var/safety_sound_custom = FALSE
// Action vars
/// Ref to any active thrusters we might have
var/obj/item/mecha_parts/mecha_equipment/thrusters/active_thrusters
/// Bool for leg overload on/off
VAR_FINAL/overclock_mode = FALSE
/// Whether it is possible to toggle overclocking from the cabin
var/can_use_overclock = FALSE
/// Speed and energy usage modifier for leg overload
var/overclock_coeff = 1.5
/// Current leg actuator temperature. Increases when overloaded, decreases when not.
var/overclock_temp = 0
/// Temperature threshold at which actuators may start causing internal damage
var/overclock_temp_danger = 15
/// Whether the mech has an option to enable safe overclocking
var/overclock_safety_available = FALSE
/// Whether the overclocking turns off automatically when overheated
var/overclock_safety = FALSE
/// Action type for overclocking, if null - no action
var/overclock_action_type = /datum/action/vehicle/sealed/mecha/mech_overclock
/// Name of the overclock action
var/overclock_name = "overclock"
/// Whether we are strafing
var/strafe = FALSE
/// Ref to screen object that displays in the middle of the UI
var/atom/movable/screen/map_view/ui_view
/// Theme of the mech TGUI
var/ui_theme = "ntos"
/// Module selected by default when mech UI is opened
var/ui_selected_module_index
/// Whether we've been knocked down, unable to move
var/toppled = FALSE
/datum/armor/sealed_mecha
melee = 20
bullet = 10
bomb = 10
fire = 100
acid = 100
/obj/vehicle/sealed/mecha/Initialize(mapload, built_manually)
. = ..()
ui_view = new()
ui_view.generate_view("mech_view_[REF(src)]")
RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
RegisterSignal(src, COMSIG_LIGHT_EATER_ACT, PROC_REF(on_light_eater))
spark_system = new(src, 2, FALSE)
spark_system.attach(src)
cabin_air = new(cabin_volume)
if(!built_manually)
populate_parts()
update_access()
set_wires(new /datum/wires/mecha(src))
START_PROCESSING(SSobj, src)
SSpoints_of_interest.make_point_of_interest(src)
log_message("[src.name] created.", LOG_MECHA)
GLOB.mechas_list += src //global mech list
prepare_huds()
var/datum/atom_hud/data/diagnostic/diag_hud = GLOB.huds[DATA_HUD_DIAGNOSTIC]
diag_hud.add_atom_to_hud(src)
diag_hud_set_mechhealth()
diag_hud_set_mechcell()
diag_hud_set_mechstat()
update_appearance()
AddElement(/datum/element/atmos_sensitive, mapload)
become_hearing_sensitive(trait_source = ROUNDSTART_TRAIT)
add_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE), ROUNDSTART_TRAIT) //stormy weather (keeps rainin' all the time)
for(var/key in equip_by_category)
if(key == MECHA_L_ARM || key == MECHA_R_ARM)
var/path = equip_by_category[key]
if(!path)
continue
var/obj/item/mecha_parts/mecha_equipment/thing = new path
thing.attach(src, key == MECHA_R_ARM)
continue
for(var/path in equip_by_category[key])
var/obj/item/mecha_parts/mecha_equipment/thing = new path
thing.attach(src, FALSE)
equip_by_category[key] -= path
AddElement(/datum/element/falling_hazard, damage = 80, wound_bonus = 10, hardhat_safety = FALSE, crushes = TRUE)
AddElement(/datum/element/hostile_machine)
/obj/vehicle/sealed/mecha/Destroy()
// If the former occupants get polymorphed, mutated, chestburstered,
// or otherwise replaced by another mob, that mob is no longer in .occupants
// and gets deleted with the mech. However, they do remain in .contents
var/list/potential_occupants = contents | occupants
for(var/mob/buggy_ejectee in potential_occupants)
mob_exit(buggy_ejectee, silent = TRUE, forced = TRUE)
if(LAZYLEN(flat_equipment))
for(var/obj/item/mecha_parts/mecha_equipment/equip as anything in flat_equipment)
equip.detach(loc)
qdel(equip)
STOP_PROCESSING(SSobj, src)
LAZYCLEARLIST(flat_equipment)
QDEL_NULL(ore_box)
QDEL_NULL(cell)
QDEL_NULL(scanmod)
QDEL_NULL(capacitor)
QDEL_NULL(servo)
QDEL_NULL(cabin_air)
QDEL_NULL(spark_system)
QDEL_NULL(ui_view)
QDEL_LIST(trackers)
QDEL_NULL(chassis_camera)
GLOB.mechas_list -= src //global mech list
var/datum/atom_hud/data/diagnostic/diag_hud = GLOB.huds[DATA_HUD_DIAGNOSTIC]
diag_hud.remove_atom_from_hud(src) //YEET
return ..()
/// Add parts on mech spawning. Skipped in manual construction.
/obj/vehicle/sealed/mecha/proc/populate_parts()
cell = new /obj/item/stock_parts/power_store/cell/high(src)
scanmod = new /obj/item/stock_parts/scanning_module(src)
capacitor = new /obj/item/stock_parts/capacitor(src)
servo = new /obj/item/stock_parts/servo(src)
update_part_values()
/obj/vehicle/sealed/mecha/proc/locate_parts()
cell = locate(/obj/item/stock_parts/power_store) in contents
diag_hud_set_mechcell()
scanmod = locate(/obj/item/stock_parts/scanning_module) in contents
capacitor = locate(/obj/item/stock_parts/capacitor) in contents
servo = locate(/obj/item/stock_parts/servo) in contents
update_part_values()
/obj/vehicle/sealed/mecha/atom_destruction()
spark_system?.start()
loc.assume_air(cabin_air)
var/mob/living/silicon/ai/unlucky_ai
for(var/mob/living/occupant as anything in occupants)
if(!isAI(occupant))
mob_exit(occupant, forced = TRUE)
if(!isbrain(occupant)) // who would win.. 1 brain vs 1 sleep proc..
occupant.SetSleeping(destruction_sleep_duration)
continue
var/mob/living/silicon/ai/ai = occupant
if(ai.linked_core || ai.can_shunt) // we probably shouldnt gib AIs with a core or shunting abilities
mob_exit(ai, silent = TRUE, forced = TRUE) // so we dont ghost the AI
continue
unlucky_ai = occupant
ai.investigate_log("has been gibbed by having their mech destroyed.", INVESTIGATE_DEATHS)
ai.gib(DROP_ALL_REMAINS) //No wreck, no AI to recover
if(ore_box)
INVOKE_ASYNC(ore_box, TYPE_PROC_REF(/obj/structure/ore_box, dump_box_contents))
if(!wreckage)
return ..()
var/obj/structure/mecha_wreckage/wreck = new wreckage(loc, unlucky_ai)
for(var/obj/item/mecha_parts/mecha_equipment/equipment in flat_equipment)
if(equipment.detachable && prob(30))
wreck.crowbar_salvage += equipment
equipment.detach(wreck) //detaches from src into wreck
equipment.active = TRUE
else
equipment.detach(loc)
qdel(equipment)
if(cell)
wreck.crowbar_salvage += cell
cell.forceMove(wreck)
cell.use(rand(0, cell.charge), TRUE)
cell = null
/obj/vehicle/sealed/mecha/update_icon_state()
icon_state = get_mecha_occupancy_state()
return ..()
/**
* Toggles Weapons Safety
*
* Handles enabling or disabling the safety function.
*/
/obj/vehicle/sealed/mecha/proc/set_safety(mob/user)
weapons_safety = !weapons_safety
if(!safety_sound_custom)
SEND_SOUND(user, sound('sound/machines/beep/beep.ogg', volume = 25))
balloon_alert(user, "equipment [weapons_safety ? "safe" : "ready"]")
set_mouse_pointer()
SEND_SIGNAL(src, COMSIG_MECH_SAFETIES_TOGGLE, user, weapons_safety)
/**
* Updates the pilot's mouse cursor override.
*
* If the mech's weapons safety is enabled, there should be no override, and the user gets their regular mouse cursor. If safety
* is off but the mech's equipment is disabled (such as by EMP), the cursor should be the red disabled version. Otherwise, if
* safety is off and the equipment is functional, the cursor should be the regular green cursor. This proc sets the cursor.
* correct and then updates it for each mob in the occupants list.
*/
/obj/vehicle/sealed/mecha/proc/set_mouse_pointer()
if(weapons_safety)
mouse_pointer = ""
else
if(equipment_disabled)
mouse_pointer = 'icons/effects/mouse_pointers/mecha_mouse-disable.dmi'
else
mouse_pointer = 'icons/effects/mouse_pointers/mecha_mouse.dmi'
for(var/mob/mob_occupant as anything in occupants)
mob_occupant.update_mouse_pointer()
//override this proc if you need to split up mecha control between multiple people (see savannah_ivanov.dm)
/obj/vehicle/sealed/mecha/auto_assign_occupant_flags(mob/occupant)
if(driver_amount() < max_drivers)
add_control_flags(occupant, FULL_MECHA_CONTROL)
/obj/vehicle/sealed/mecha/generate_actions()
initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/mech_eject)
if(mecha_flags & IS_ENCLOSED)
initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_toggle_cabin_seal, VEHICLE_CONTROL_SETTINGS)
if(can_use_overclock && overclock_action_type)
initialize_passenger_action_type(overclock_action_type)
initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_toggle_lights, VEHICLE_CONTROL_SETTINGS)
initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_toggle_safeties, VEHICLE_CONTROL_SETTINGS)
initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_view_stats, VEHICLE_CONTROL_SETTINGS)
initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/strafe, VEHICLE_CONTROL_DRIVE)
/obj/vehicle/sealed/mecha/add_occupant(mob/M, control_flags, forced)
. = ..()
if(.)
generate_equipment_actions(M)
/obj/vehicle/sealed/mecha/remove_occupant(mob/M)
remove_all_equipment_actions(M)
return ..()
/// Generates action buttons for all eligible equipment and grants them to the occupant with VEHICLE_CONTROL_SETTINGS flag.
/obj/vehicle/sealed/mecha/proc/generate_equipment_actions(mob/occupant)
if(!(occupant in occupants) || !(occupants[occupant] & VEHICLE_CONTROL_SETTINGS))
return
for(var/obj/item/mecha_parts/mecha_equipment/equipment in flat_equipment)
if(!is_equipment_valid_for_action(equipment))
continue
grant_equipment_action(occupant, equipment)
/// Removes all equipment actions from a specific occupant.
/obj/vehicle/sealed/mecha/proc/remove_all_equipment_actions(mob/occupant)
var/list/actions = LAZYACCESS(occupant_actions, occupant)
if(!actions)
return
for(var/equipment_type in actions)
if(!ispath(equipment_type, /obj/item/mecha_parts/mecha_equipment))
continue
remove_action_type_from_mob(equipment_type, occupant)
/**
* Grants a specific equipment action to an occupant.
* Creates a new action, sets up the chassis and equipment references, and grants it to the mob.
*/
/obj/vehicle/sealed/mecha/proc/grant_equipment_action(mob/occupant, obj/item/mecha_parts/mecha_equipment/equipment)
var/datum/action/vehicle/sealed/mecha/equipment/action = new equipment.action_type // We cannot use grant_action_type_to_mob() because:
action.set_chassis(src) // 1. grant_action_type_to_mob() works with a single predefined action type
action.set_equipment(equipment) // 2. We create unique action instances for each equipment with specific equipment references
action.Grant(occupant)
LAZYINITLIST(occupant_actions[occupant])
// Use equipment type as actiontype for remove_action_type_from_mob() compatibility
occupant_actions[occupant][equipment.type] = action
/**
* Called when equipment is attached to the mecha.
* Grants equipment actions to current occupants with VEHICLE_CONTROL_SETTINGS flag.
*/
/obj/vehicle/sealed/mecha/proc/on_equipment_attach(obj/item/mecha_parts/mecha_equipment/equipment)
if(!is_equipment_valid_for_action(equipment))
return
for(var/mob/occupant in occupants)
if(!(occupants[occupant] & VEHICLE_CONTROL_SETTINGS))
continue
grant_equipment_action(occupant, equipment)
/**
* Called when equipment is detached from the mecha.
* Removes equipment actions from all current occupants.
*/
/obj/vehicle/sealed/mecha/proc/on_equipment_detach(obj/item/mecha_parts/mecha_equipment/equipment)
for(var/mob/occupant in occupants)
remove_action_type_from_mob(equipment.type, occupant)
/// Create actions only for equipment that can be toggled or triggered, excluding air tanks.
/obj/vehicle/sealed/mecha/proc/is_equipment_valid_for_action(obj/item/mecha_parts/mecha_equipment/equipment)
if(!(equipment.can_be_toggled || equipment.can_be_triggered))
return FALSE
if(istype(equipment, /obj/item/mecha_parts/mecha_equipment/air_tank)) // this thing has its own button
return FALSE
return TRUE
/obj/vehicle/sealed/mecha/proc/get_mecha_occupancy_state()
if((mecha_flags & SILICON_PILOT) && silicon_icon_state)
return silicon_icon_state
if(LAZYLEN(occupants))
return base_icon_state
return "[base_icon_state]-open"
/obj/vehicle/sealed/mecha/get_cell()
return cell
/obj/vehicle/sealed/mecha/rust_heretic_act()
take_damage(500, BRUTE)
/obj/vehicle/sealed/mecha/proc/restore_equipment()
equipment_disabled = FALSE
for(var/occupant in occupants)
var/mob/mob_occupant = occupant
SEND_SOUND(mob_occupant, sound('sound/items/timer.ogg', volume=50))
to_chat(mob_occupant, span_notice("Equipment control unit has been rebooted successfully."))
set_mouse_pointer()
/// Updates the values given by scanning module and capacitor tier, called when a part is removed or inserted.
/obj/vehicle/sealed/mecha/proc/update_part_values()
update_energy_drain()
if(capacitor)
overclock_temp_danger = initial(overclock_temp_danger) * capacitor.rating
else
overclock_temp_danger = initial(overclock_temp_danger)
/obj/vehicle/sealed/mecha/examine(mob/user)
. = ..()
if(LAZYLEN(flat_equipment))
. += span_notice("It's equipped with:")
for(var/obj/item/mecha_parts/mecha_equipment/ME as anything in flat_equipment)
if(istype(ME, /obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay))
continue
. += span_notice("[icon2html(ME, user)] \A [ME].")
if(mecha_flags & PANEL_OPEN)
if(servo)
. += span_notice("Servo reduces movement power usage by [100 - round(100 / servo.rating)]%")
else
. += span_warning("It's missing a servo.")
if(capacitor)
. += span_notice("Capacitor increases armor against energy attacks by [capacitor.rating * 5].")
else
. += span_warning("It's missing a capacitor.")
if(!scanmod)
. += span_warning("It's missing a scanning module.")
if(!(mecha_flags & IS_ENCLOSED))
if(mecha_flags & SILICON_PILOT)
. += span_notice("[src] appears to be piloting itself...")
else
for(var/occupante in occupants)
. += span_notice("You can see [occupante] inside.")
if(ishuman(user))
var/mob/living/carbon/human/H = user
for(var/held_item in H.held_items)
if(!isgun(held_item))
continue
. += span_warning("It looks like you can hit the pilot directly if you target the center or above.")
break //in case user is holding two guns
. += span_notice("It has a <a href='byond://?src=[REF(src)];list_armor=1'>tag</a> listing its protection classes.")
/obj/vehicle/sealed/mecha/Topic(href, href_list)
. = ..()
if(href_list["list_armor"])
var/list/readout = list()
var/datum/armor/armor = get_armor()
var/added_damage_header = FALSE
for(var/damage_key in ARMOR_LIST_DAMAGE)
var/rating = armor.get_rating(damage_key)
if(!rating)
continue
if(!added_damage_header)
readout += "<b><u>ARMOR (I-X)</u></b>"
added_damage_header = TRUE
readout += "[armor_to_protection_name(damage_key)] [armor_to_protection_class(rating)]"
var/added_durability_header = FALSE
for(var/durability_key in ARMOR_LIST_DURABILITY)
var/rating = armor.get_rating(durability_key)
if(!rating)
continue
if(!added_durability_header)
readout += "<b><u>DURABILITY (I-X)</u></b>"
added_durability_header = TRUE
readout += "[armor_to_protection_name(durability_key)] [armor_to_protection_class(rating)]"
readout += "It can withstand temperatures up to [max_temperature]K."
if(mecha_flags & IS_ENCLOSED)
readout += "It fully encloses its occupants, protecting them from the atmosphere or lack thereof."
var/formatted_readout = span_notice("<b>PROTECTION CLASSES</b><hr>[jointext(readout, "\n")]")
to_chat(usr, boxed_message(formatted_readout))
/obj/vehicle/sealed/mecha/generate_integrity_message()
var/examine_text = ""
var/integrity = atom_integrity / max_integrity * 100
switch(integrity)
if(85 to 100)
examine_text = "It's fully intact."
if(65 to 85)
examine_text = "It's slightly damaged."
if(45 to 65)
examine_text = "It's badly damaged."
if(25 to 45)
examine_text = "It's heavily damaged."
else
examine_text = "It's falling apart."
return examine_text
/// Locate an internal tack in the utility modules
/obj/vehicle/sealed/mecha/proc/get_internal_tank()
var/obj/item/mecha_parts/mecha_equipment/air_tank/module = locate(/obj/item/mecha_parts/mecha_equipment/air_tank) in equip_by_category[MECHA_UTILITY]
return module?.internal_tank
//processing internal damage, temperature, air regulation, alert updates, lights power use.
/obj/vehicle/sealed/mecha/process(seconds_per_tick)
if(overclock_mode || overclock_temp > 0)
process_overclock_effects(seconds_per_tick)
if(internal_damage)
process_internal_damage_effects(seconds_per_tick)
if(cabin_sealed)
process_cabin_air(seconds_per_tick)
if(length(occupants))
process_occupants(seconds_per_tick)
process_constant_power_usage(seconds_per_tick)
//Diagnostic HUD updates
diag_hud_set_mechhealth()
diag_hud_set_mechcell()
diag_hud_set_mechstat()
/obj/vehicle/sealed/mecha/proc/process_overclock_effects(seconds_per_tick)
if(!overclock_mode && overclock_temp > 0)
overclock_temp -= seconds_per_tick
return
var/temp_gain = seconds_per_tick * (1 + 1 / movedelay)
overclock_temp = min(overclock_temp + temp_gain, overclock_temp_danger * 2)
if(overclock_temp < overclock_temp_danger)
return
if(overclock_temp >= overclock_temp_danger && overclock_safety)
toggle_overclock(FALSE)
return
var/damage_chance = 100 * ((overclock_temp - overclock_temp_danger) / (overclock_temp_danger * 2))
if(SPT_PROB(damage_chance, seconds_per_tick))
do_sparks(5, TRUE, src)
try_deal_internal_damage(damage_chance)
take_damage(seconds_per_tick, BURN, 0, 0)
/obj/vehicle/sealed/mecha/proc/process_internal_damage_effects(seconds_per_tick)
if(internal_damage & MECHA_INT_FIRE)
if(!(internal_damage & MECHA_INT_TEMP_CONTROL) && SPT_PROB(2.5, seconds_per_tick))
clear_internal_damage(MECHA_INT_FIRE)
if(cabin_air && cabin_sealed && cabin_air.return_volume()>0)
if(cabin_air.return_pressure() > (PUMP_DEFAULT_PRESSURE * 30) && !(internal_damage & MECHA_CABIN_AIR_BREACH))
set_internal_damage(MECHA_CABIN_AIR_BREACH)
cabin_air.temperature = min(6000+T0C, cabin_air.temperature+rand(5,7.5)*seconds_per_tick)
if(cabin_air.return_temperature() > max_temperature/2)
take_damage(seconds_per_tick*2/round(max_temperature/cabin_air.return_temperature(),0.1), BURN, 0, 0)
if(internal_damage & MECHA_CABIN_AIR_BREACH && cabin_air && cabin_sealed) //remove some air from cabin_air
var/datum/gas_mixture/leaked_gas = cabin_air.remove_ratio(SPT_PROB_RATE(0.05, seconds_per_tick))
if(loc)
loc.assume_air(leaked_gas)
else
qdel(leaked_gas)
if(internal_damage & MECHA_INT_SHORT_CIRCUIT && get_charge())
spark_system.start()
var/damage_energy_consumption = 0.005 * STANDARD_CELL_CHARGE * seconds_per_tick
use_energy(damage_energy_consumption)
cell.maxcharge -= min(damage_energy_consumption, cell.maxcharge)
/obj/vehicle/sealed/mecha/proc/process_cabin_air(seconds_per_tick)
if(!(internal_damage & MECHA_INT_TEMP_CONTROL) && cabin_air && cabin_air.return_volume() > 0)
var/heat_capacity = cabin_air.heat_capacity()
var/required_energy = abs(T20C - cabin_air.temperature) * heat_capacity
required_energy = min(required_energy, 1000)
if(required_energy < 1)
return
var/delta_temperature = required_energy / heat_capacity
if(delta_temperature)
if(cabin_air.temperature < T20C)
cabin_air.temperature += delta_temperature
else
cabin_air.temperature -= delta_temperature
/obj/vehicle/sealed/mecha/proc/process_occupants(seconds_per_tick)
for(var/mob/living/occupant as anything in occupants)
if(!(mecha_flags & IS_ENCLOSED) && occupant?.incapacitated) //no sides mean it's easy to just sorta fall out if you're incapacitated.
mob_exit(occupant, randomstep = TRUE) //bye bye
continue
if(cell && cell.maxcharge)
var/cellcharge = cell.charge/cell.maxcharge
switch(cellcharge)
if(0.75 to INFINITY)
occupant.clear_alert(ALERT_CHARGE)
if(0.5 to 0.75)
occupant.throw_alert(ALERT_CHARGE, /atom/movable/screen/alert/lowcell/mech, 1)
if(0.25 to 0.5)
occupant.throw_alert(ALERT_CHARGE, /atom/movable/screen/alert/lowcell/mech, 2)
if(0.01 to 0.25)
occupant.throw_alert(ALERT_CHARGE, /atom/movable/screen/alert/lowcell/mech, 3)
else
occupant.throw_alert(ALERT_CHARGE, /atom/movable/screen/alert/emptycell/mech)
else
occupant.throw_alert(ALERT_CHARGE, /atom/movable/screen/alert/nocell)
var/integrity = atom_integrity/max_integrity*100
switch(integrity)
if(30 to 45)
occupant.throw_alert(ALERT_MECH_DAMAGE, /atom/movable/screen/alert/low_mech_integrity, 1)
if(15 to 35)
occupant.throw_alert(ALERT_MECH_DAMAGE, /atom/movable/screen/alert/low_mech_integrity, 2)
if(-INFINITY to 15)
occupant.throw_alert(ALERT_MECH_DAMAGE, /atom/movable/screen/alert/low_mech_integrity, 3)
else
occupant.clear_alert(ALERT_MECH_DAMAGE)
var/atom/checking = occupant.loc
// recursive check to handle all cases regarding very nested occupants,
// such as brainmob inside brainitem inside MMI inside mecha
while(!isnull(checking))
if(isturf(checking))
// hit a turf before hitting the mecha, seems like they have been moved out
occupant.clear_alert(ALERT_CHARGE)
occupant.clear_alert(ALERT_MECH_DAMAGE)
occupant = null
break
else if (checking == src)
break // all good
checking = checking.loc
/obj/vehicle/sealed/mecha/proc/process_constant_power_usage(seconds_per_tick)
if(mecha_flags & LIGHTS_ON && !use_energy(light_power_drain * seconds_per_tick))
mecha_flags &= ~LIGHTS_ON
set_light_on(mecha_flags & LIGHTS_ON)
playsound(src,'sound/machines/clockcult/brass_skewer.ogg', 40, TRUE)
log_message("Toggled lights off due to the lack of power.", LOG_MECHA)
/// Called when a driver clicks somewhere. Handles everything like equipment, punches, etc.
/obj/vehicle/sealed/mecha/proc/on_mouseclick(mob/user, atom/target, list/modifiers)
SIGNAL_HANDLER
if(LAZYACCESS(modifiers, MIDDLE_CLICK))
set_safety(user)
return COMSIG_MOB_CANCEL_CLICKON
if(weapons_safety)
return
if(isAI(user)) //For AIs: If safeties are off, use mech functions. If safeties are on, use AI functions.
. = COMSIG_MOB_CANCEL_CLICKON
if(modifiers[SHIFT_CLICK]) //Allows things to be examined.
return
if(!isturf(target) && !isturf(target.loc)) // Prevents inventory from being drilled
return
if(!can_interact_with(target, user, modifiers))
return
if(src == target)
return
var/dir_to_target = get_dir(src,target)
if(!(mecha_flags & OMNIDIRECTIONAL_ATTACKS) && dir_to_target && !(dir_to_target & dir))//wrong direction
return
if(internal_damage & MECHA_INT_CONTROL_LOST)
target = pick(view(3,target))
var/mob/living/livinguser = user
if(!(livinguser in return_controllers_with_flag(VEHICLE_CONTROL_EQUIPMENT)))
balloon_alert(user, "wrong seat for equipment!")
return
var/obj/item/mecha_parts/mecha_equipment/selected
if(modifiers[BUTTON] == RIGHT_CLICK)
selected = equip_by_category[MECHA_R_ARM]
else
selected = equip_by_category[MECHA_L_ARM]
if(selected)
if(!Adjacent(target) && (selected.range & MECHA_RANGED))
if(HAS_TRAIT(livinguser, TRAIT_PACIFISM) && selected.harmful)
to_chat(livinguser, span_warning("You don't want to harm other living beings!"))
return
if(SEND_SIGNAL(src, COMSIG_MECHA_EQUIPMENT_CLICK, livinguser, target) & COMPONENT_CANCEL_EQUIPMENT_CLICK)
return
INVOKE_ASYNC(selected, TYPE_PROC_REF(/obj/item/mecha_parts/mecha_equipment, action), user, target, modifiers)
return
if(Adjacent(target) && (selected.range & MECHA_MELEE))
if(isliving(target) && selected.harmful && HAS_TRAIT(livinguser, TRAIT_PACIFISM))
to_chat(livinguser, span_warning("You don't want to harm other living beings!"))
return
if(SEND_SIGNAL(src, COMSIG_MECHA_EQUIPMENT_CLICK, livinguser, target) & COMPONENT_CANCEL_EQUIPMENT_CLICK)
return
INVOKE_ASYNC(selected, TYPE_PROC_REF(/obj/item/mecha_parts/mecha_equipment, action), user, target, modifiers)
return
if(!(livinguser in return_controllers_with_flag(VEHICLE_CONTROL_MELEE)))
to_chat(livinguser, span_warning("You're in the wrong seat to interact with your hands."))
return
var/on_cooldown = TIMER_COOLDOWN_RUNNING(src, COOLDOWN_MECHA_MELEE_ATTACK)
var/adjacent = Adjacent(target)
if(SEND_SIGNAL(src, COMSIG_MECHA_MELEE_CLICK, livinguser, target, on_cooldown, adjacent) & COMPONENT_CANCEL_MELEE_CLICK)
return
if(on_cooldown || !adjacent)
return
if(internal_damage & MECHA_INT_CONTROL_LOST)
target = pick(oview(1,src))
if(!has_charge(melee_energy_drain))
return
use_energy(melee_energy_drain)
SEND_SIGNAL(user, COMSIG_MOB_USED_CLICK_MECH_MELEE, src)
if(target.mech_melee_attack(src, user))
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MELEE_ATTACK, melee_cooldown)
/obj/vehicle/sealed/mecha/proc/can_interact_with(atom/target, mob/user, list/modifiers)
if(completely_disabled || is_currently_ejecting || (mecha_flags & CANNOT_INTERACT))
return FALSE
if(user.incapacitated)
return FALSE
if(!get_charge())
return FALSE
return TRUE
/// Driver alt clicks anything while in mech
/obj/vehicle/sealed/mecha/proc/on_click_alt(mob/user, atom/target, params)
SIGNAL_HANDLER
. = COMSIG_MOB_CANCEL_CLICKON // Cancel base_click_alt
if(target != src)
return
if(!(user in occupants))
return
if(!(user in return_controllers_with_flag(VEHICLE_CONTROL_DRIVE)))
to_chat(user, span_warning("You're in the wrong seat to control movement."))
return
toggle_strafe()
/// Middle mouse click signal wrapper for AI users
/obj/vehicle/sealed/mecha/proc/on_middlemouseclick(mob/user, atom/target, params)
SIGNAL_HANDLER
if(isAI(user))
on_mouseclick(user, target, params)
/// Displays a special speech bubble when someone inside the mecha speaks
/obj/vehicle/sealed/mecha/proc/display_speech_bubble(datum/source, list/speech_args)
SIGNAL_HANDLER
var/list/speech_bubble_recipients = list()
for(var/mob/listener in get_hearers_in_view(7, src))
if(listener.client)
speech_bubble_recipients += listener.client
var/image/mech_speech = image('icons/mob/effects/talk.dmi', src, "machine[say_test(speech_args[SPEECH_MESSAGE])]",MOB_LAYER+1)
INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(flick_overlay_global), mech_speech, speech_bubble_recipients, 3 SECONDS)
// Atmospheric stuff
/obj/vehicle/sealed/mecha/remove_air(amount)
if((mecha_flags & IS_ENCLOSED) && cabin_sealed)
return cabin_air.remove(amount)
return ..()
/obj/vehicle/sealed/mecha/return_air()
if((mecha_flags & IS_ENCLOSED) && cabin_sealed)
return cabin_air
return ..()
/obj/vehicle/sealed/mecha/return_analyzable_air()
return cabin_air
/// Fetches pressure of the gas mixture we are using
/obj/vehicle/sealed/mecha/proc/return_pressure()
var/datum/gas_mixture/air = return_air()
return air?.return_pressure()
/// Fetches temp of the gas mixture we are using
/obj/vehicle/sealed/mecha/return_temperature()
var/datum/gas_mixture/air = return_air()
return air?.return_temperature()
/// Makes cabin unsealed, dumping cabin air outside or airtight filling the cabin with external air mix
/obj/vehicle/sealed/mecha/proc/set_cabin_seal(mob/user, cabin_sealed)
if(!(mecha_flags & IS_ENCLOSED))
balloon_alert(user, "cabin can't be sealed!")
log_message("Tried to seal cabin. This mech can't be airtight.", LOG_MECHA)
return
if(TIMER_COOLDOWN_RUNNING(src, COOLDOWN_MECHA_CABIN_SEAL))
balloon_alert(user, "on cooldown!")
return
TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_CABIN_SEAL, 1 SECONDS)
src.cabin_sealed = cabin_sealed
var/datum/gas_mixture/environment_air = loc.return_air()
if(!isnull(environment_air))
if(cabin_sealed)
// Fill cabin with air
environment_air.pump_gas_to(cabin_air, environment_air.return_pressure())
else
// Dump cabin air
var/datum/gas_mixture/removed_gases = cabin_air.remove_ratio(1)
if(loc)
loc.assume_air(removed_gases)
else
qdel(removed_gases)
var/obj/item/mecha_parts/mecha_equipment/air_tank/tank = locate(/obj/item/mecha_parts/mecha_equipment/air_tank) in equip_by_category[MECHA_UTILITY]
for(var/mob/occupant as anything in occupants)
var/datum/action/action = locate(/datum/action/vehicle/sealed/mecha/mech_toggle_cabin_seal) in occupant.actions
if(!isnull(tank) && cabin_sealed && tank.auto_pressurize_on_seal)
if(!tank.active)
tank.set_active(TRUE)
else
action.button_icon_state = "mech_cabin_pressurized"
action.build_all_button_icons()
else
action.button_icon_state = "mech_cabin_[cabin_sealed ? "closed" : "open"]"
action.build_all_button_icons()
balloon_alert(occupant, "cabin [cabin_sealed ? "sealed" : "unsealed"]")
log_message("Cabin [cabin_sealed ? "sealed" : "unsealed"].", LOG_MECHA)
playsound(src, 'sound/machines/airlock/airlock.ogg', 50, TRUE)
/// Special light eater handling
/obj/vehicle/sealed/mecha/proc/on_light_eater(obj/vehicle/sealed/source, datum/light_eater)
SIGNAL_HANDLER
if(mecha_flags & HAS_LIGHTS)
visible_message(span_danger("[src]'s lights burn out!"))
mecha_flags &= ~HAS_LIGHTS
set_light_on(FALSE)
for(var/occupant in occupants)
remove_action_type_from_mob(/datum/action/vehicle/sealed/mecha/mech_toggle_lights, occupant)
return COMPONENT_BLOCK_LIGHT_EATER
/obj/vehicle/sealed/mecha/on_saboteur(datum/source, disrupt_duration)
. = ..()
if((mecha_flags & HAS_LIGHTS) && light_on)
set_light_on(FALSE)
return TRUE
/// Apply corresponding accesses
/obj/vehicle/sealed/mecha/proc/update_access()
req_access = one_access ? list() : accesses
req_one_access = one_access ? accesses : list()
/// Electrocute user from power celll
/obj/vehicle/sealed/mecha/shock(mob/living/shocking, chance = 100, shock_source, siemens_coeff)
if(get_charge() < 1)
return FALSE
if(isnull(siemens_coeff))
siemens_coeff = 0.7
return ..()
/// Toggle mech overclock with a button or by hacking
/obj/vehicle/sealed/mecha/proc/toggle_overclock(forced_state = null)
if(!isnull(forced_state))
if(overclock_mode == forced_state)
return FALSE
overclock_mode = forced_state
else
overclock_mode = !overclock_mode
log_message("Toggled [overclock_name].", LOG_MECHA)
for(var/mob/occupant as anything in occupants)
balloon_alert(occupant, "[overclock_name] [overclock_mode ? "on":"off"]")
var/datum/action/act = locate(/datum/action/vehicle/sealed/mecha/mech_overclock) in occupant.actions
act?.build_all_button_icons(UPDATE_BUTTON_ICON)
if(overclock_mode)
movedelay /= overclock_coeff
visible_message(span_notice("[src] starts heating up, making humming sounds."))
else
movedelay *= overclock_coeff
visible_message(span_notice("[src] cools down and the humming stops."))
update_energy_drain()
return TRUE
/// Update the energy drain according to parts and status
/obj/vehicle/sealed/mecha/proc/update_energy_drain()
if(servo)
step_energy_drain = initial(step_energy_drain) / servo.rating
else
step_energy_drain = 2 * initial(step_energy_drain)
if(overclock_mode)
step_energy_drain *= overclock_coeff
if(capacitor)
melee_energy_drain = initial(melee_energy_drain) / capacitor.rating
light_power_drain = initial(light_power_drain) / capacitor.rating
else
melee_energy_drain = initial(melee_energy_drain)
light_power_drain = initial(light_power_drain)
/// Toggle lights on/off
/obj/vehicle/sealed/mecha/proc/toggle_lights(forced_state = null, mob/user)
if(!(mecha_flags & HAS_LIGHTS))
if(user)
balloon_alert(user, "mech has no lights!")
return
if((!(mecha_flags & LIGHTS_ON) && forced_state != FALSE) && get_charge() < power_to_energy(light_power_drain, scheduler = SSobj))
if(user)
balloon_alert(user, "no power for lights!")
return
mecha_flags ^= LIGHTS_ON
set_light_on(mecha_flags & LIGHTS_ON)
playsound(src,'sound/machines/clockcult/brass_skewer.ogg', 40, TRUE)
log_message("Toggled lights [(mecha_flags & LIGHTS_ON)?"on":"off"].", LOG_MECHA)
for(var/mob/occupant as anything in occupants)
var/datum/action/act = locate(/datum/action/vehicle/sealed/mecha/mech_toggle_lights) in occupant.actions
if(mecha_flags & LIGHTS_ON)
act.button_icon_state = "mech_lights_on"
else
act.button_icon_state = "mech_lights_off"
balloon_alert(occupant, "lights [mecha_flags & LIGHTS_ON ? "on":"off"]")
act.build_all_button_icons()
/obj/vehicle/sealed/mecha/proc/melee_attack_effect(mob/living/victim, heavy)
if(heavy)
victim.Unconscious(2 SECONDS)
else
victim.Knockdown(4 SECONDS)