/***************** 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 ///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 ///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_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" //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(src, 2, FALSE) spark_system.attach(src) smoke_system = new(src, 3, holder = 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 && 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/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 tag 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 += "ARMOR (I-X)" 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 += "DURABILITY (I-X)" 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("PROTECTION CLASSES