Files
tonty 8c45d4466c [MDB IGNORE] Cleans up AI core structure, un-spaghettis some AI code (#93873)
## About The Pull Request

The AI core structure now fully integrates item/tool interactions and no
longer uses attackby/take_damage overrides. Construction behaviour has
been isolated into its own file.

All handling relating to the AI core's status when shunted has been
turned into signals. For this reason, helper procs for managing core
link state have been made and integrated into the relevant users (APC
shunting, mech takeover). The AI core no longer references the remote AI
mob.

/obj/structure/ai_core/deactivated has been removed from code since it
had no function after refactoring. There was only a single instance of
it being used in a map, which has been replaced with a var edit.

There may be more things I changed, but I'm h
2025-12-02 12:38:51 -06:00

1030 lines
39 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/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
///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
///Portable camera camerachunk update
var/updating = FALSE
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
///Handles an internal ore box for mining mechs
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
var/datum/effect_system/fluid_spread/smoke/smoke_system
////Action vars
///Ref to any active thrusters we might have
var/obj/item/mecha_parts/mecha_equipment/thrusters/active_thrusters
///Bool for energy shield on/off
var/defense_mode = FALSE
///Bool for leg overload on/off
var/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
//Bool for zoom on/off
var/zoom_mode = FALSE
///Remaining smoke charges
var/smoke_charges = 5
///Cooldown between using smoke
var/smoke_cooldown = 10 SECONDS
///check for phasing, if it is set to text (to describe how it is phasing: "flying", "phasing") it will let the mech walk through walls.
var/phasing = ""
///Power we use every time we phaze through something
var/phasing_energy_drain = 0.2 * STANDARD_CELL_CHARGE
///icon_state for flick() when phazing
var/phase_state = ""
///Wether we are strafing
var/strafe = FALSE
///Bool for whether this mech can only be used on lavaland
var/lavaland_only = 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
/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
spark_system.set_up(2, 0, src)
spark_system.attach(src)
smoke_system = new
smoke_system.set_up(3, holder = src, location = src)
smoke_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(smoke_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))
//FIXME: Nothiing about this block works
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
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
else
mob_exit(ai, silent = TRUE, forced = TRUE) // so we dont ghost the AI
continue
else
mob_exit(occupant, forced = TRUE)
if(!isbrain(occupant)) // who would win.. 1 brain vs 1 sleep proc..
occupant.SetSleeping(destruction_sleep_duration)
if(wreckage)
var/obj/structure/mecha_wreckage/WR = new wreckage(loc, unlucky_ai)
for(var/obj/item/mecha_parts/mecha_equipment/E in flat_equipment)
if(E.detachable && prob(30))
WR.crowbar_salvage += E
E.detach(WR) //detaches from src into WR
E.active = TRUE
else
E.detach(loc)
qdel(E)
if(cell)
WR.crowbar_salvage += cell
cell.forceMove(WR)
cell.use(rand(0, cell.charge), TRUE)
cell = null
return ..()
/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/M)
if(driver_amount() < max_drivers)
add_control_flags(M, 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)
initialize_passenger_action_type(/datum/action/vehicle/sealed/mecha/mech_overclock)
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/CanPassThrough(atom/blocker, movement_dir, blocker_opinion)
if(!phasing || get_charge() <= phasing_energy_drain || throwing)
return ..()
if(phase_state)
flick(phase_state, src)
var/turf/destination_turf = get_step(loc, movement_dir)
if(!check_teleport_valid(src, destination_turf) || SSmapping.level_trait(destination_turf.z, ZTRAIT_NOPHASE))
return FALSE
return TRUE
/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()
/obj/vehicle/sealed/mecha/proc/update_part_values() ///Updates the values given by scanning module and capacitor tier, called when a part is removed or inserted.
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*100/max_integrity
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(completely_disabled || is_currently_ejecting || (mecha_flags & CANNOT_INTERACT))
return
if(phasing)
balloon_alert(user, "not while [phasing]!")
return
if(user.incapacitated)
return
if(!get_charge())
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)
/// 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/proc/shock(mob/living/user)
if(!istype(user) || get_charge() < 1)
return FALSE
do_sparks(5, TRUE, src)
return electrocute_mob(user, cell, src, 0.7, TRUE)
/// 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
overclock_mode = forced_state
else
overclock_mode = !overclock_mode
log_message("Toggled overclocking.", LOG_MECHA)
for(var/mob/occupant as anything in occupants)
var/datum/action/act = locate(/datum/action/vehicle/sealed/mecha/mech_overclock) in occupant.actions
if(!act)
continue
act.button_icon_state = "mech_overload_[overclock_mode ? "on" : "off"]"
balloon_alert(occupant, "overclock [overclock_mode ? "on":"off"]")
act.build_all_button_icons()
if(overclock_mode)
movedelay = movedelay / overclock_coeff
visible_message(span_notice("[src] starts heating up, making humming sounds."))
else
movedelay = initial(movedelay)
visible_message(span_notice("[src] cools down and the humming stops."))
update_energy_drain()
/// 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)
phasing_energy_drain = initial(phasing_energy_drain) / capacitor.rating
melee_energy_drain = initial(melee_energy_drain) / capacitor.rating
light_power_drain = initial(light_power_drain) / capacitor.rating
else
phasing_energy_drain = initial(phasing_energy_drain)
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)