diff --git a/code/__DEFINES/mob.dm b/code/__DEFINES/mob.dm index 119fdf33ad09..4073a4168a20 100644 --- a/code/__DEFINES/mob.dm +++ b/code/__DEFINES/mob.dm @@ -93,6 +93,7 @@ #define SKELETON_VOX "Skeleton Vox" #define SHADOWLING "Shadowling" #define GOLEM "Adamantine Golem" +#define BLUESPACE "Bluespace Being" #define HOMUNCULUS "Homunculus" #define ZOMBIE "Zombie" #define ZOMBIE_TAJARAN "Zombie Tajaran" diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm index 506eb3b9a0a4..53360d3c0a49 100644 --- a/code/_onclick/ai.dm +++ b/code/_onclick/ai.dm @@ -10,7 +10,7 @@ Note that AI have no need for the adjacency proc, and so this proc is a lot cleaner. */ /mob/living/silicon/ai/DblClickOn(atom/A, params) - if(client.click_intercept) // handled in normal click. + if(client && client.click_intercept) // handled in normal click. return if(control_disabled || stat != CONSCIOUS) return diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 7d2fc491753c..4461a9462ab4 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -60,13 +60,13 @@ if(notransform) return - if(client.click_intercept) + if(client && client.click_intercept) client.click_intercept.InterceptClickOn(src, params, A) return var/list/modifiers = params2list(params) - if(client.cob && client.cob.in_building_mode) + if(client && client.cob && client.cob.in_building_mode) cob_click(client, modifiers) return @@ -312,7 +312,8 @@ user.listed_turf = null else user.listed_turf = T - user.client.statpanel = T.name + if(user.client) + user.client.statpanel = T.name /mob/living/AltClick(mob/living/user) /* diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm index 784bced88e4a..be76de1a3c5d 100644 --- a/code/_onclick/cyborg.dm +++ b/code/_onclick/cyborg.dm @@ -11,13 +11,13 @@ return next_click = world.time + 1 - if(client.click_intercept) // comes after object.Click to allow buildmode gui objects to be clicked + if(client && client.click_intercept) // comes after object.Click to allow buildmode gui objects to be clicked client.click_intercept.InterceptClickOn(src, params, A) return var/list/modifiers = params2list(params) - if(client.cob.in_building_mode) + if(client && client.cob.in_building_mode) cob_click(client, modifiers) return diff --git a/code/datums/browser.dm b/code/datums/browser.dm index a02dd5fdfd27..71214e5d922c 100644 --- a/code/datums/browser.dm +++ b/code/datums/browser.dm @@ -17,6 +17,10 @@ var/mob/M = nuser nuser = M.client + if(!nuser) + qdel(src) + return + user = nuser LAZYSET(user.browsers, nwindow_id, src) window_id = nwindow_id @@ -92,6 +96,9 @@ "} /datum/browser/proc/open() + if(!user) + return + var/window_size if(width && height) window_size = "size=[width]x[height];" diff --git a/code/datums/wires/_wires.dm b/code/datums/wires/_wires.dm index 95b02e9f2b1d..13a8d3a55858 100644 --- a/code/datums/wires/_wires.dm +++ b/code/datums/wires/_wires.dm @@ -346,6 +346,13 @@ var/global/list/wire_daltonism_colors = list() else CRASH("[color] is not a key in wires.") +/datum/wires/proc/get_color_by_index(index) + for(var/col in wires) + var/other_index = wires[col] + if(index == other_index) + return col + + CRASH("[index] is not an index in wires") //////////////////////////// // Is Index/Colour Cut procs diff --git a/code/datums/wires/manipulator.dm b/code/datums/wires/manipulator.dm new file mode 100644 index 000000000000..2558946bf7f2 --- /dev/null +++ b/code/datums/wires/manipulator.dm @@ -0,0 +1,52 @@ +var/global/const/MANIPULATOR_WIRE_ACTIVATE = 1 +var/global/const/MANIPULATOR_WIRE_AFTER_ACTIVATE = 2 +var/global/const/MANIPULATOR_WIRE_CHANGE_TARGET_ZONE = 4 +var/global/const/MANIPULATOR_WIRE_ATTACK_SELF_ON_INTERACTION = 8 +var/global/const/MANIPULATOR_WIRE_AUTO_ACTIVATION = 16 + +/datum/wires/manipulator + holder_type = /obj/machinery/manipulator + wire_count = 5 + window_y = 560 + +/datum/wires/manipulator/can_use() + var/obj/machinery/manipulator/M = holder + return M.panel_open + +/datum/wires/manipulator/get_status() + var/obj/machinery/manipulator/M = holder + . += ..() + . += "Target selection screen displays: [parse_zone(M.target_zone)]" + . += "The 'Activate Instead' light is [M.attack_self_interaction ? "on" : "off"]." + . += "The 'Auto Activation' light is [M.auto_activation ? "on" : "off"]." + +/datum/wires/manipulator/update_cut(index, mended) + var/obj/machinery/manipulator/M = holder + + switch(index) + if(MANIPULATOR_WIRE_AUTO_ACTIVATION) + M.auto_activation = mended + return + + if(MANIPULATOR_WIRE_ATTACK_SELF_ON_INTERACTION) + M.attack_self_interaction = !mended + return + + update_pulsed(index) + +/datum/wires/manipulator/update_pulsed(index) + var/obj/machinery/manipulator/M = holder + + switch(index) + if(MANIPULATOR_WIRE_ACTIVATE) + if(!M.can_activate()) + return + + M.forced = TRUE + M.activate() + + if(MANIPULATOR_WIRE_CHANGE_TARGET_ZONE) + M.cycle_target_zone() + + if(MANIPULATOR_WIRE_ATTACK_SELF_ON_INTERACTION) + M.next_attack_self_interaction = TRUE diff --git a/code/game/atoms.dm b/code/game/atoms.dm index f9696f54e2d7..257212e24acf 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -336,6 +336,8 @@ //called to set the atom's dir and used to add behaviour to dir-changes /atom/proc/set_dir(new_dir) + SHOULD_CALL_PARENT(TRUE) + . = new_dir != dir dir = new_dir if(.) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 5e2c1a77159a..ede0a138cfa9 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -459,6 +459,8 @@ /client/var/list/image/outlined_item = list() /atom/movable/proc/apply_outline(color) + if(!usr || !usr.client) + return if(anchored || !usr.client.prefs.outline_enabled) return if(!color) @@ -479,6 +481,8 @@ /atom/movable/proc/remove_outline() + if(!usr || !usr.client) + return usr.client.images -= usr.client.outlined_item[src] usr.client.outlined_item -= src diff --git a/code/game/machinery/autolathe.dm b/code/game/machinery/autolathe.dm index 2f855de8a7be..059924b6bd28 100644 --- a/code/game/machinery/autolathe.dm +++ b/code/game/machinery/autolathe.dm @@ -78,6 +78,7 @@ var/global/list/datum/autolathe_recipe/autolathe_recipes = list( R(/obj/item/weapon/storage/pill_bottle, CATEGORY_MEDICAL), R(/obj/item/stack/cable_coil/random, CATEGORY_ENGINEERING), R(/obj/item/weapon/module/power_control, CATEGORY_ENGINEERING), + R(/obj/item/weapon/circuitboard/manipulator, CATEGORY_ENGINEERING), R(/obj/item/weapon/airlock_electronics, CATEGORY_ENGINEERING), R(/obj/item/weapon/airalarm_electronics, CATEGORY_ENGINEERING), R(/obj/item/weapon/firealarm_electronics, CATEGORY_ENGINEERING), diff --git a/code/game/machinery/constructable_frame.dm b/code/game/machinery/constructable_frame.dm index ddfde8949227..9880bc72d553 100644 --- a/code/game/machinery/constructable_frame.dm +++ b/code/game/machinery/constructable_frame.dm @@ -123,6 +123,9 @@ if(!anchored) to_chat(user, "The frame needs to be secured first!") return + if(issilicon(user) && istype(P, /obj/item/weapon/circuitboard/manipulator)) + to_chat(user, "Sorry! Automatic protocols preventing AI from becoming too powerful can't allow you to assemble this!") + return var/obj/item/weapon/circuitboard/B = P if(B.board_type == "machine") if(!user.drop_from_inventory(P, src)) diff --git a/code/game/machinery/manipulator.dm b/code/game/machinery/manipulator.dm new file mode 100644 index 000000000000..c29cbd745d53 --- /dev/null +++ b/code/game/machinery/manipulator.dm @@ -0,0 +1,772 @@ +/* + ✞ + Abandon hope. + ✞ + + Отче наш, сущий на небесах! + Да святится имя Твоё; + да приидет Царствие Твоё; + да будет воля Твоя и на земле, + как на небе; + хлеб наш насущный дай нам на сей день; + и прости нам долги наши, + как и мы прощаем должникам нашим; + и не введи нас в искушение, но избавь нас от лукавого. + Ибо Твоё есть Царство и сила и слава во веки. + Аминь. + — Мф. 6:9—13 + + Manipulator is a machinery that simulates clicking stuff on stuff. + Currently it creates it's own mob to click stuff with. + Which is I might say. Sinful. + */ + +#define MANIPULATOR_STATE_OFF "off" +#define MANIPULATOR_STATE_IDLE "idle" +#define MANIPULATOR_STATE_FAIL "fail" +#define MANIPULATOR_STATE_INTERACTING_FROM "interacting_from" +#define MANIPULATOR_STATE_INTERACTING_TO "interacting_to" + +/mob/living/carbon/human/bluespace + var/target_zone + + var/datum/skills/skills + +/mob/living/carbon/human/bluespace/atom_init() + . = ..() + skills = new + skills.add_available_skillset(/datum/skillset/max) + skills.maximize_active_skills() + +/mob/living/carbon/human/bluespace/get_targetzone() + if(zone_sel) + return ..() + return target_zone + +/mob/living/carbon/human/bluespace/SetNextMove(num) + return + +/mob/living/carbon/human/bluespace/AdjustNextMove(num) + return + +/mob/living/carbon/human/bluespace/get_skills() + return skills + +/obj/item/weapon/circuitboard/manipulator + name = "Circuit board (Manipulator)" + build_path = /obj/machinery/manipulator + origin_tech = "programming=3;materials=3;engineering=3" + board_type = "machine" + + req_components = list( + /obj/item/weapon/stock_parts/manipulator = 1, + /obj/item/weapon/stock_parts/capacitor = 1, + /obj/item/weapon/stock_parts/micro_laser = 1, + ) + + m_amt = 50 + g_amt = 50 + +/obj/machinery/manipulator + name = "manipulator" + desc = "Manipulates stuff. I think we'll put this thing right here..." + + icon = 'icons/obj/machines/logistic.dmi' + icon_state = "base" + + anchored = TRUE + + use_power = IDLE_POWER_USE + idle_power_usage = 10 + active_power_usage = 100 + + var/delay = 0 + + var/turf/from_turf + var/turf/to_turf + + var/turf/fail_turf + + var/mob/living/carbon/human/bluespace/clicker + + var/state = MANIPULATOR_STATE_IDLE + + var/mirrored = FALSE + var/fail_angle = 90 + + var/image/decal + var/image/panel + var/image/status + var/image/eyes + var/atom/movable/hand + var/atom/movable/item + + var/item_x = 0 + var/item_y = 15 + var/item_scale = 0.75 + + // Initialized in atom_init because BYOND is weird with int assoc lists like that. + var/list/hand_offset + + var/busy_moving = FALSE + + // This is here solely for the coolness of manipulators opening crates. + // If something enters the tile even when manipulator is working, it will remember it, + // and activate whenver it stops being busy. + var/remember_trigger = FALSE + + var/datum/wires/manipulator/wires + + var/target_zone = "random" + + var/static/list/possible_target_zones = TARGET_ZONE_ALL + list("random") + + // Whether the next activation chain should be forced (i.e. even when a manipulator doesn't find an item to click with). + var/forced = FALSE + + // Whether instead of clicking to something we should click the item in hand. + var/attack_self_interaction = FALSE + // When pulsed attack self interact only once. + var/next_attack_self_interaction = FALSE + + // Whether the manipulator will automatically activate on items entering the turf. + var/auto_activation = TRUE + + // Whether we are cute and have googly eyes. + var/has_eyes = FALSE + // The idea of the googly eyes icon. + var/eyes_id = 1 + // Is currently shaking eyes. + var/shaking_eyes = FALSE + +/obj/machinery/manipulator/atom_init() + . = ..() + + hand_offset = list( + "[NORTH]" = list(-1, 4), + "[SOUTH]" = list(0, 0), + "[WEST]" = list(-2, 2), + "[EAST]" = list(2, 3), + ) + + wires = new(src) + + var/image/I = image(icon, src, "manipulator", layer + 0.1, dir) + + hand = new(null) + hand.simulated = FALSE + hand.anchored = TRUE + hand.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + hand.appearance = I + + vis_contents += hand + + eyes_id = rand(1, 3) + + decal = image(icon, src, "manip_decor", layer, dir) + panel = image(icon, src, "base-wires", layer, dir) + status = image(icon, src, "power_on", layer, dir) + eyes = image(icon, src, "eyes-[eyes_id]", layer, dir) + + add_overlay(decal) + + create_clicker() + + component_parts = list() + component_parts += new /obj/item/weapon/circuitboard/manipulator(null) + component_parts += new /obj/item/weapon/stock_parts/manipulator(null) + component_parts += new /obj/item/weapon/stock_parts/capacitor(null) + component_parts += new /obj/item/weapon/stock_parts/micro_laser(null) + + RefreshParts() + + set_dir(dir) + + if(is_operational() && !panel_open) + add_overlay(status) + +/obj/machinery/manipulator/RefreshParts() + delay = 6 + + for(var/obj/item/weapon/stock_parts/manipulator/M in component_parts) + delay -= M.rating + + // Min delay between clicks is 1, so we need at least 2 tick delay to actually be able to interact. + delay = max(delay, 2) + + var/laser_rating = 1 + + for(var/obj/item/weapon/stock_parts/micro_laser/M in component_parts) + laser_rating += M.rating + + clicker.mood_multiplicative_actionspeed_modifier = max(-1.0 / (15.0 - laser_rating), -0.9) + +/obj/machinery/manipulator/Destroy() + QDEL_NULL(clicker) + + vis_contents -= hand + if(item) + hand.vis_contents -= item + + cut_overlay(decal) + cut_overlay(panel) + cut_overlay(status) + overlays -= eyes + + QDEL_NULL(hand) + QDEL_NULL(item) + QDEL_NULL(decal) + QDEL_NULL(eyes) + QDEL_NULL(panel) + QDEL_NULL(status) + + from_turf = null + to_turf = null + fail_turf = null + + return ..() + +/obj/machinery/manipulator/proc/do_sleep(_delay, datum/callback/extra_checks=null) + busy_moving = TRUE + + var/endtime = world.time + _delay + + . = TRUE + + while(world.time < endtime) + stoplag() + if(QDELETED(src)) + . = FALSE + break + + if(extra_checks && !extra_checks.Invoke()) + . = FALSE + break + + busy_moving = FALSE + +/obj/machinery/manipulator/proc/set_mirrored(mirrored) + src.mirrored = mirrored + + if(mirrored) + fail_angle = -90 + else + fail_angle = 90 + + if(fail_turf) + UnregisterSignal(fail_turf, list(COMSIG_PARENT_QDELETING)) + set_fail_turf() + + decal.icon_state = "manip_decor[mirrored ? "-mirrored" : ""]" + cut_overlay(decal) + add_overlay(decal) + +/obj/machinery/manipulator/power_change() + ..() + if(is_operational() && !panel_open) + add_overlay(status) + +/obj/machinery/manipulator/update_icon() + if(!clicker) + return + + var/obj/item/I = clicker.get_active_hand() + if(!I) + if(item) + hand.vis_contents -= item + item = null + return + + var/image/IM = image(I.icon, src, I.icon_state, layer + 0.11, SOUTH) + + var/matrix/M = matrix() + M.Scale(item_scale, item_scale) + + IM.transform = M + + if(item) + return + + item = new(null) + item.simulated = FALSE + item.anchored = TRUE + + item.pixel_x = item_x + item.pixel_y = item_y + + item.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + + item.appearance = IM + item.appearance_flags |= KEEP_TOGETHER + + hand.vis_contents += item + +/obj/machinery/manipulator/proc/get_hand_angle() + var/hand_angle = 0 + switch(state) + if(MANIPULATOR_STATE_IDLE) + hand_angle = 0 // -fail_angle if you want this state to be visible too. + if(MANIPULATOR_STATE_FAIL) + hand_angle = fail_angle + if(MANIPULATOR_STATE_INTERACTING_FROM) + hand_angle = 0 + if(MANIPULATOR_STATE_INTERACTING_TO) + hand_angle = 180 + + // -180 because manipulator is on the opposite side of where the base is facing. + return dir2angle(dir) - 180 + hand_angle + +/obj/machinery/manipulator/proc/set_state(new_state) + if(state == new_state) + return + + state = new_state + + var/hand_angle = get_hand_angle() + + var/matrix/M = matrix() + M.Turn(hand_angle) + + update_icon() + + animate(hand, time=delay, transform=M) + if(item) + var/item_angle = hand_angle + + var/matrix/MI = matrix() + + MI.Turn(item_angle) + MI.Scale(item_scale, item_scale) + + var/p_x = item_x + hand.pixel_x + var/p_y = item_y + hand.pixel_y + + var/x = p_x * cos(item_angle) + p_y * sin(item_angle) + var/y = -p_x * sin(item_angle) + p_y * cos(item_angle) + animate(item, time=delay, pixel_x=x, pixel_y=y, transform=MI) + + use_power(active_power_usage) + +/obj/machinery/manipulator/proc/set_from_turf() + SIGNAL_HANDLER + var/opposite_dir = turn(dir, 180) + from_turf = get_step(src, opposite_dir) + RegisterSignal(from_turf, list(COMSIG_ATOM_ENTERED), .proc/on_from_entered) + RegisterSignal(from_turf, list(COMSIG_PARENT_QDELETING), .proc/set_from_turf) + +/obj/machinery/manipulator/proc/set_fail_turf() + SIGNAL_HANDLER + var/fail_dir = turn(dir, fail_angle) + fail_turf = get_step(src, fail_dir) + RegisterSignal(fail_turf, list(COMSIG_PARENT_QDELETING), .proc/set_fail_turf) + +/obj/machinery/manipulator/proc/set_to_turf() + SIGNAL_HANDLER + to_turf = get_step(src, dir) + RegisterSignal(to_turf, list(COMSIG_PARENT_QDELETING), .proc/set_to_turf) + +/obj/machinery/manipulator/set_dir(new_dir) + . = ..() + + if(from_turf) + UnregisterSignal(from_turf, list(COMSIG_ATOM_ENTERED, COMSIG_PARENT_QDELETING)) + if(fail_turf) + UnregisterSignal(fail_turf, list(COMSIG_PARENT_QDELETING)) + if(to_turf) + UnregisterSignal(to_turf, list(COMSIG_PARENT_QDELETING)) + + set_from_turf() + set_fail_turf() + set_to_turf() + + var/string_dir = "[dir]" + hand.pixel_x = hand_offset[string_dir][1] + hand.pixel_y = hand_offset[string_dir][2] + var/matrix/M = matrix() + M.Turn(get_hand_angle()) + hand.transform = M + + decal.dir = dir + cut_overlay(decal) + add_overlay(decal) + +/obj/machinery/manipulator/is_operational() + return ..() && anchored + +/obj/machinery/manipulator/proc/on_from_entered(datum/source, atom/movable/entering, atom/oldLoc) + SIGNAL_HANDLER + + if(!auto_activation) + return + + if(!can_activate(entering)) + remember_trigger = TRUE + return + + activate(entering) + +/obj/machinery/manipulator/proc/can_activate(atom/target=null) + if(!is_operational()) + return FALSE + + if(state != MANIPULATOR_STATE_IDLE) + return FALSE + + if(busy_moving) + return FALSE + + return TRUE + +/obj/machinery/manipulator/proc/activate(atom/target=null) + INVOKE_ASYNC(src, .proc/try_interact_from, target) + +/obj/machinery/manipulator/proc/after_activate() + var/obj/item/device/assembly/signaler/S = wires.get_attached_signaler( + wires.get_color_by_index(MANIPULATOR_WIRE_AFTER_ACTIVATE) + ) + if(S) + S.signal() + + for(var/obj/item/I in clicker) + clicker.drop_from_inventory(I, loc) + +/obj/machinery/manipulator/verb/rotate() + set category = "Object" + set name = "Rotate" + set desc = "Rotate the manipulator." + set src in oview(1) + + if(usr.incapacitated()) + return + + if(state != MANIPULATOR_STATE_IDLE) + to_chat(usr, "You cannot rotate [src] while it's working.") + return + + if(busy_moving) + to_chat(usr, "You cannot rotate [src] while it's working.") + return + + playsound(src, 'sound/items/Ratchet.ogg', VOL_EFFECTS_MASTER) + set_dir(turn(dir, -90)) + to_chat(usr, "You rotate [src].") + +/obj/machinery/manipulator/verb/mirror() + set category = "Object" + set name = "Mirror" + set desc = "Mirror the manipulator." + set src in oview(1) + + if(usr.incapacitated()) + return + + if(state != MANIPULATOR_STATE_IDLE) + to_chat(usr, "You cannot mirror [src] while it's working.") + return + + if(busy_moving) + to_chat(usr, "You cannot mirror [src] while it's working.") + return + + playsound(src, 'sound/items/Ratchet.ogg', VOL_EFFECTS_MASTER) + set_mirrored(!mirrored) + to_chat(usr, "You mirror [src].") + +/obj/machinery/manipulator/attack_hand(mob/user) + if(wires.interact(user)) + return + + return ..() + +/obj/machinery/manipulator/attackby(obj/item/I, mob/user, params) + if(isscrewing(I)) + panel_open = !panel_open + if(panel_open) + if(is_operational()) + cut_overlay(status) + add_overlay(panel) + else + if(is_operational()) + add_overlay(status) + cut_overlay(panel) + + else if(iscutter(I)) + wires.interact(user) + return + + else if(ispulsing(I)) + wires.interact(user) + return + + else if(issignaler(I)) + wires.interact(user) + return + + else if(default_unfasten_wrench(user, I) && !busy_moving && state == MANIPULATOR_STATE_IDLE) + if(!panel_open) + if(anchored) + add_overlay(stat) + set_dir(dir) + else + cut_overlay(stat) + return + + else if(istype(I, /obj/item/weapon/pen)) + to_chat(user, "You draw googly eyes on the chassis of [src]. Somehow they even shake!") + overlays += eyes + has_eyes = TRUE + return + + else if(default_deconstruction_crowbar(I)) + return + + return ..() + +/obj/machinery/manipulator/default_unfasten_wrench(mob/user, obj/item/weapon/I, time = SKILL_TASK_VERY_EASY) + if(issilicon(user)) + to_chat(user, "Sorry! Automatic protocols preventing AI from becoming too powerful can't allow you to unwrench this!") + return FALSE + + if(state != MANIPULATOR_STATE_IDLE) + to_chat(user, "You cannot unwrench [src] while it's working.") + return FALSE + + if(busy_moving) + to_chat(user, "You cannot unwrench [src] while it's working.") + return FALSE + + return ..() + +/obj/machinery/manipulator/proc/create_clicker() + clicker = new /mob/living/carbon/human/bluespace(src) + clicker.simulated = FALSE + clicker.real_name = "manipulator ([rand(0, 999)])" + clicker.name = clicker.real_name + clicker.status_flags |= GODMODE + clicker.canmove = FALSE + clicker.invisibility = INVISIBILITY_ABSTRACT + clicker.anchored = TRUE + clicker.density = FALSE + clicker.layer = BELOW_TURF_LAYER + clicker.plane = CLICKCATCHER_PLANE + + global.mob_list -= clicker + global.living_list -= clicker + global.carbon_list -= clicker + global.human_list -= clicker + global.alive_mob_list -= clicker + +/obj/machinery/manipulator/proc/before_click() + if(name != "manipulator") + clicker.real_name = name + clicker.name = clicker.real_name + + clicker.rejuvenate() + clicker.target_zone = target_zone + if(target_zone == "random") + clicker.target_zone = pick(TARGET_ZONE_ALL) + clicker.forceMove(loc) + +/obj/machinery/manipulator/proc/after_click() + clicker.forceMove(src) + +/obj/machinery/manipulator/proc/clickability_from(atom/movable/A) + return !A.anchored + +/obj/machinery/manipulator/proc/clickability_to(atom/A) + return TRUE + +/obj/machinery/manipulator/proc/find_clickable(turf/T, datum/callback/clickability) + if(!T.contents.len) + return null + + var/atom/most_clickable + + for(var/C in T.contents) + var/atom/movable/A = C + + if(A.name == "") + continue + + if(!A.simulated) + continue + + if(A.invisibility > clicker.see_invisible) + continue + + if(clickability && !clickability.Invoke(A)) + continue + + if(!most_clickable) + most_clickable = A + continue + + if(A.plane > most_clickable.plane) + most_clickable = A + + else if(A.plane == most_clickable.plane && A.layer > most_clickable.layer) + most_clickable = A + + return most_clickable + +/obj/machinery/manipulator/proc/DoClick(atom/A, list/params) + if(!A) + return + usr = clicker + clicker.ClickOn(A, params) + +/obj/machinery/manipulator/proc/ClickAndCallBack(atom/A, list/params, list/datum/callback/callbacks) + DoClick(A, params) + + after_click() + + for(var/datum/callback/C in callbacks) + C.Invoke() + +/obj/machinery/manipulator/proc/simulate_click(atom/A, list/datum/callback/callbacks) + var/static/list/fake_params = "[ICON_X]=16&[ICON_Y]=16" + + before_click() + + INVOKE_ASYNC(src, .proc/ClickAndCallBack, A, fake_params, callbacks) + +/obj/machinery/manipulator/proc/after_interact_from() + var/obj/item/I = clicker.get_active_hand() + if(!I && !forced) + if(remember_trigger) + remember_trigger = FALSE + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + try_interact_from() + return + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + after_activate() + return + + forced = FALSE + + try_interact_to() + +/obj/machinery/manipulator/proc/shake_eyes() + if(shaking_eyes) + return + shaking_eyes = TRUE + + var/list/eyes_recipients = list() + for(var/mob/M in viewers(src)) + if(M.client) + eyes_recipients += M.client + + var/image/I = image(icon, src, "eyes-[eyes_id]-anim", layer, dir) + I.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + flick_overlay(I, eyes_recipients, 5) + + eyes_id = rand(1, 3) + overlays -= eyes + eyes = image(icon, src, "eyes-[eyes_id]", layer, dir) + overlays += eyes + + VARSET_IN(src, shaking_eyes, FALSE, 5) + +/obj/machinery/manipulator/proc/try_interact_from(atom/target=null) + if(!target) + target = find_clickable(from_turf) + + if(!target && !forced) + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + return + + set_state(MANIPULATOR_STATE_INTERACTING_FROM) + if(!do_sleep(delay, CALLBACK(src, /obj/machinery.proc/is_operational))) + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + return + + if(has_eyes) + shake_eyes() + + simulate_click(target, list(CALLBACK(src, .proc/after_interact_from))) + +/obj/machinery/manipulator/proc/after_interact_to() + var/obj/item/I = clicker.get_active_hand() + if(I) + set_state(MANIPULATOR_STATE_FAIL) + do_sleep(delay) + + clicker.drop_from_inventory(I, fail_turf) + + set_state(MANIPULATOR_STATE_INTERACTING_TO) + do_sleep(delay) + + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + after_activate() + try_interact_from() + +/obj/machinery/manipulator/proc/try_interact_to(atom/target=null) + if(!target) + target = find_clickable(to_turf) + + if(!target && (attack_self_interaction || next_attack_self_interaction)) + target = clicker.get_active_hand() + next_attack_self_interaction = FALSE + + if(!target) + var/obj/item/I = clicker.get_active_hand() + if(I) + set_state(MANIPULATOR_STATE_INTERACTING_TO) + if(!do_sleep(delay, CALLBACK(src, /obj/machinery.proc/is_operational))) + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + after_activate() + return + + if(QDELETED(I)) + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + after_activate() + return + + clicker.drop_from_inventory(I, to_turf) + + INVOKE_ASYNC(src, .proc/after_interact_to) + return + + set_state(MANIPULATOR_STATE_INTERACTING_TO) + if(!do_sleep(delay, CALLBACK(src, /obj/machinery.proc/is_operational))) + set_state(MANIPULATOR_STATE_IDLE) + do_sleep(delay) + after_activate() + return + + simulate_click(target, list(CALLBACK(src, .proc/after_interact_to))) + +/obj/machinery/manipulator/proc/cycle_target_zone() + var/cur_target_index = possible_target_zones.Find(target_zone) + cur_target_index += 1 + + if(cur_target_index > length(possible_target_zones)) + cur_target_index -= length(possible_target_zones) + + target_zone = possible_target_zones[cur_target_index] + +/obj/machinery/manipulator/emag_act(mob/user) + if(emagged) + return FALSE + clicker.a_intent = INTENT_HARM + to_chat(user, "You short out [src]'s harm prevention systems.") + visible_message("[src] hums oddly...") + emagged = TRUE + return TRUE + +/obj/machinery/manipulator/Moved(atom/OldLoc, Dir) + . = ..() + if(has_eyes) + shake_eyes() + +#undef MANIPULATOR_STATE_IDLE +#undef MANIPULATOR_STATE_FAIL +#undef MANIPULATOR_STATE_INTERACTING_FROM +#undef MANIPULATOR_STATE_INTERACTING_TO diff --git a/code/game/objects/items/mines.dm b/code/game/objects/items/mines.dm index 515f097c24e8..c8d57f133d99 100644 --- a/code/game/objects/items/mines.dm +++ b/code/game/objects/items/mines.dm @@ -55,6 +55,8 @@ qdel(src) /obj/item/mine/proc/try_trigger(atom/movable/AM) + if(!AM.simulated) + return if(isliving(AM) || istype(AM, /obj/mecha)) if(anchored) AM.visible_message("[AM] steps on [src]!") diff --git a/code/modules/demo/hooks.dm b/code/modules/demo/hooks.dm index fec1ed78f5c9..e5c6a43d35c5 100644 --- a/code/modules/demo/hooks.dm +++ b/code/modules/demo/hooks.dm @@ -15,10 +15,10 @@ . = ..() SSdemo.write_event_line("logout [ckey]") -/turf/set_dir() +/turf/set_dir(new_dir) . = ..() SSdemo.mark_turf(src) -/atom/movable/set_dir() +/atom/movable/set_dir(new_dir) . = ..() SSdemo.mark_dirty(src) diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 87cf2499d252..862bec536f1b 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -127,7 +127,7 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images //this is called when a ghost is drag clicked to something. /mob/dead/observer/MouseDrop(atom/over) if(!usr || !over) return - if (isobserver(usr) && usr.client.holder && isliving(over)) + if (isobserver(usr) && usr.client && usr.client.holder && isliving(over)) if (usr.client.holder.cmd_ghost_drag(src,over)) return diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 4da4bb83960f..adca3c3b11bf 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -125,6 +125,9 @@ /mob/living/carbon/human/golem/atom_init(mapload) . = ..(mapload, GOLEM) +/mob/living/carbon/human/bluespace/atom_init(mapload) + . = ..(mapload, BLUESPACE) + /mob/living/carbon/human/shadowling/atom_init(mapload) . = ..(mapload, SHADOWLING) var/newNameId = pick(possibleShadowlingNames) diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index 3615dcfe00ce..7c357f41909b 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -10,6 +10,8 @@ return if(!loc) return // Fixing a null error that occurs when the mob isn't found in the world -- TLE + if(!simulated) + return ..() diff --git a/code/modules/mob/living/carbon/human/update_icons.dm b/code/modules/mob/living/carbon/human/update_icons.dm index 1f7f956e12a9..3993228d115a 100644 --- a/code/modules/mob/living/carbon/human/update_icons.dm +++ b/code/modules/mob/living/carbon/human/update_icons.dm @@ -171,6 +171,9 @@ Please contact me on #coderbus IRC. ~Carn x //BASE MOB SPRITE /mob/living/carbon/human/proc/update_body() + if(!simulated) + return + remove_standing_overlay(BODY_LAYER) var/fat = HAS_TRAIT(src, TRAIT_FAT) ? "fat" : null @@ -247,6 +250,9 @@ Please contact me on #coderbus IRC. ~Carn x //HAIR OVERLAY /mob/living/carbon/human/proc/update_hair() + if(!simulated) + return + //Reset our hair remove_standing_overlay(HAIR_LAYER) @@ -304,6 +310,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_mutations() + if(!simulated) + return + remove_standing_overlay(MUTATIONS_LAYER) var/list/standing = list() @@ -341,6 +350,9 @@ Please contact me on #coderbus IRC. ~Carn x //Call when target overlay should be added/removed /mob/living/carbon/human/update_targeted() + if(!simulated) + return + remove_standing_overlay(TARGETED_LAYER) if(targeted_by && target_locked) @@ -352,6 +364,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_fire() //TG-stuff, fire layer + if(!simulated) + return + remove_standing_overlay(FIRE_LOWER_LAYER) remove_standing_overlay(FIRE_UPPER_LAYER) @@ -371,6 +386,9 @@ Please contact me on #coderbus IRC. ~Carn x /* --------------------------------------- */ //For legacy support. /mob/living/carbon/human/regenerate_icons() + if(!simulated) + return + ..() if(notransform) return @@ -408,6 +426,9 @@ Please contact me on #coderbus IRC. ~Carn x //vvvvvv UPDATE_INV PROCS vvvvvv /mob/living/carbon/human/update_inv_w_uniform() + if(!simulated) + return + remove_standing_overlay(UNIFORM_LAYER) var/default_path = 'icons/mob/uniform.dmi' @@ -462,6 +483,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_wear_id() + if(!simulated) + return + remove_standing_overlay(ID_LAYER) if(wear_id) wear_id.screen_loc = ui_id @@ -476,6 +500,9 @@ Please contact me on #coderbus IRC. ~Carn x apply_standing_overlay(ID_LAYER) /mob/living/carbon/human/update_inv_gloves() + if(!simulated) + return + remove_standing_overlay(GLOVES_LAYER) if(gloves) if(client && hud_used && hud_used.hud_shown) @@ -501,6 +528,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_glasses() + if(!simulated) + return + remove_standing_overlay(GLASSES_LAYER) if(glasses) @@ -519,6 +549,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_ears() + if(!simulated) + return + remove_standing_overlay(EARS_LAYER) if(l_ear || r_ear) @@ -549,6 +582,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_shoes() + if(!simulated) + return + remove_standing_overlay(SHOES_LAYER) if(shoes) @@ -575,6 +611,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_s_store() + if(!simulated) + return + remove_standing_overlay(SUIT_STORE_LAYER) if(s_store) @@ -595,6 +634,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_head() + if(!simulated) + return + remove_standing_overlay(HEAD_LAYER) if(head) @@ -625,6 +667,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_belt() + if(!simulated) + return + remove_standing_overlay(BELT_LAYER) if(belt) @@ -641,6 +686,9 @@ Please contact me on #coderbus IRC. ~Carn x apply_standing_overlay(BELT_LAYER) /mob/living/carbon/human/update_inv_wear_suit() + if(!simulated) + return + remove_standing_overlay(SUIT_LAYER) var/default_path = 'icons/mob/suit.dmi' @@ -693,6 +741,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_pockets() + if(!simulated) + return + if(l_store) l_store.screen_loc = ui_storage1 if(client && hud_used) @@ -704,6 +755,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_wear_mask() + if(!simulated) + return + remove_standing_overlay(FACEMASK_LAYER) if(istype(wear_mask, /obj/item/clothing/mask) || istype(wear_mask, /obj/item/clothing/accessory)) @@ -722,6 +776,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_back() + if(!simulated) + return + remove_standing_overlay(BACK_LAYER) if(back) @@ -738,6 +795,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_hud() //TODO: do away with this if possible + if(!simulated) + return + if(client) client.screen |= contents if(hud_used) @@ -746,6 +806,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_handcuffed() + if(!simulated) + return + remove_standing_overlay(HANDCUFF_LAYER) if(handcuffed) @@ -761,6 +824,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_legcuffed() + if(!simulated) + return + remove_standing_overlay(LEGCUFF_LAYER) if(legcuffed) @@ -775,6 +841,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_r_hand() + if(!simulated) + return + remove_standing_overlay(R_HAND_LAYER) if(r_hand) @@ -794,6 +863,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/update_inv_l_hand() + if(!simulated) + return + remove_standing_overlay(L_HAND_LAYER) if(l_hand) @@ -812,6 +884,9 @@ Please contact me on #coderbus IRC. ~Carn x apply_standing_overlay(L_HAND_LAYER) /mob/living/carbon/human/proc/update_tail_showing() + if(!simulated) + return + remove_standing_overlay(TAIL_LAYER) if((random_tail_holder || species.tail) && species.flags[HAS_TAIL] && !(HUSK in mutations) && bodyparts_by_name[BP_CHEST]) @@ -848,6 +923,9 @@ Please contact me on #coderbus IRC. ~Carn x //Adds a collar overlay above the helmet layer if the suit has one // Suit needs an identically named sprite in icons/mob/collar.dmi /mob/living/carbon/human/proc/update_collar() + if(!simulated) + return + remove_standing_overlay(COLLAR_LAYER) if(wear_suit) @@ -865,6 +943,9 @@ Please contact me on #coderbus IRC. ~Carn x /mob/living/carbon/human/proc/update_surgery() + if(!simulated) + return + remove_standing_overlay(SURGERY_LAYER) var/list/standing = list() @@ -880,6 +961,9 @@ Please contact me on #coderbus IRC. ~Carn x apply_standing_overlay(SURGERY_LAYER) /mob/living/carbon/human/proc/update_bandage() + if(!simulated) + return + remove_standing_overlay(BANDAGE_LAYER) var/list/standing = list() @@ -921,6 +1005,9 @@ Please contact me on #coderbus IRC. ~Carn x //Cutting any human's overlay that we dont want to offset. /mob/living/carbon/human/proc/update_height(image/I) + if(!simulated) + return + var/static/icon/cut_torso_mask = icon('icons/effects/cut.dmi',"Cut1") var/static/icon/cut_legs_mask = icon('icons/effects/cut.dmi',"Cut2") var/static/icon/lenghten_torso_mask = icon('icons/effects/cut.dmi',"Cut3") diff --git a/code/modules/mob/living/carbon/species.dm b/code/modules/mob/living/carbon/species.dm index ab0078379ab5..82904e2e3111 100644 --- a/code/modules/mob/living/carbon/species.dm +++ b/code/modules/mob/living/carbon/species.dm @@ -1454,7 +1454,7 @@ for(var/x in list(H.w_uniform, H.head, H.wear_suit, H.shoes, H.wear_mask, H.gloves)) if(x) - var/list/golem_items = list( + var/static/list/golem_items = list( /obj/item/clothing/under/golem, /obj/item/clothing/head/helmet/space/golem, /obj/item/clothing/suit/space/golem, @@ -1819,3 +1819,92 @@ H.remove_from_mob(I) H.dust() return TRUE + +/datum/species/bluespace + name = BLUESPACE + + icobase = 'icons/mob/human_races/r_golem.dmi' + deform = 'icons/mob/human_races/r_golem.dmi' + dietflags = 0 + + brute_mod = 0.0 + burn_mod = 0.0 + oxy_mod = 0.0 + tox_mod = 0.0 + clone_mod = 0.0 + brain_mod = 0.0 + + blood_datum_path = /datum/dirt_cover/oil + flesh_color = "#575757" + + butcher_drops = list(/obj/item/stack/sheet/plasteel = 3) + + flags = list( + NO_BLOOD = TRUE, + NO_DNA = TRUE, + NO_BREATHE = TRUE, + NO_SCAN = TRUE, + NO_PAIN = TRUE, + NO_EMBED = TRUE, + RAD_IMMUNE = TRUE, + VIRUS_IMMUNE = TRUE, + BIOHAZZARD_IMMUNE = TRUE, + NO_VOMIT = TRUE, + NO_FINGERPRINT = TRUE, + NO_MINORCUTS = TRUE, + NO_EMOTION = TRUE, + NO_MUTATION = TRUE, + NO_FAT = TRUE, + ) + + has_organ = list( + ) + + has_gendered_icons = FALSE + + min_age = 1 + max_age = 1000 + + // Only left and right hand are present. + restricted_inventory_slots = list( + SLOT_BACK, + SLOT_WEAR_MASK, + SLOT_HANDCUFFED, + SLOT_BELT, + SLOT_WEAR_ID, + SLOT_L_EAR, + SLOT_R_EAR, + SLOT_GLASSES, + SLOT_GLOVES, + SLOT_HEAD, + SLOT_SHOES, + SLOT_WEAR_SUIT, + SLOT_W_UNIFORM, + SLOT_L_STORE, + SLOT_R_STORE, + SLOT_S_STORE, + SLOT_IN_BACKPACK, + SLOT_LEGCUFFED, + SLOT_TIE, + SLOT_EARS, + ) + + default_mood_event = /datum/mood_event/machine + +/datum/species/bluespace/on_gain(mob/living/carbon/human/H) + ..() + // Clothing on the Bluepsace Debug Creature is created before the hud_list is generated in the atom + H.prepare_huds() + + H.status_flags &= ~(CANSTUN | CANWEAKEN | CANPARALYSE) + + qdel(H.GetComponent(/datum/component/mood)) + +/datum/species/bluespace/on_loose(mob/living/carbon/human/H) + H.status_flags |= MOB_STATUS_FLAGS_DEFAULT + H.AddComponent(/datum/component/mood) + + ..() + +/datum/species/bluespace/call_digest_proc(mob/living/M, datum/reagent/R) + return FALSE diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index c3428703338b..729bed45ab1d 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -115,7 +115,7 @@ var/global/list/department_radio_keys = list( //log var/area/A = get_area(src) - log_say("[key_name(src)] : \[[A.name][message_mode?"/[message_mode]":""]\]: [message]") + log_say("[key_name(src)] : \[[A ? A.name : "NULLSPACE"][message_mode?"/[message_mode]":""]\]: [message]") //handle nonverbal and sign languages here if (speaking) diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 14a299b112ca..cd8dd3cebd77 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -784,6 +784,8 @@ to_chat(usr, "The borg must choose a module before he can be upgraded!") else if(U.locked) to_chat(usr, "The upgrade is locked and cannot be used yet!") + else if(cell) + to_chat(usr, "Remove the power cell first.") else if(U.action(src)) to_chat(usr, "You apply the upgrade to [src]!") diff --git a/code/modules/research/designs.dm b/code/modules/research/designs.dm index 6b6092372ffe..b977929bb184 100644 --- a/code/modules/research/designs.dm +++ b/code/modules/research/designs.dm @@ -52,7 +52,7 @@ other types of metals and chemistry for reagents). category = list("Computer") /datum/design/telepad_concole - name = " Circuit Design (Telescience Console) " + name = " Circuit Design (Telescience Console)" desc = "Allows for the construction of circuit boards used to build telescience computers." id = "telepad_concole" build_type = IMPRINTER @@ -60,6 +60,15 @@ other types of metals and chemistry for reagents). build_path = /obj/item/weapon/circuitboard/telesci_console category = list("Computer") +/datum/design/manipulator + name = " Circuit Design (Manipulator)" + desc = "Allows for the construction of circuit boards used to build manipulators." + id = "manipulator" + build_type = IMPRINTER + materials = list(MAT_GLASS = 2000, "sacid" = 20) + build_path = /obj/item/weapon/circuitboard/manipulator + category = list("Machine") + /datum/design/aicore name = "Circuit Design (AI Core)" desc = "Allows for the construction of circuit boards used to build new AI cores." diff --git a/code/modules/research/research.dm b/code/modules/research/research.dm index 87603bc80bf7..5ca1e6057bbf 100644 --- a/code/modules/research/research.dm +++ b/code/modules/research/research.dm @@ -1306,7 +1306,7 @@ The tech datums are the actual "tech trees" that you improve through researching required_tech_levels = list() cost = 0 - unlocks_designs = list("mechrecharger", "cyborgrecharger", "cyborg_analyzer", "mmi", "borg_upgrade_hud") + unlocks_designs = list("mechrecharger", "cyborgrecharger", "cyborg_analyzer", "mmi", "borg_upgrade_hud", "manipulator") /datum/technology/mech_ripley name = "Ripley" diff --git a/icons/obj/machines/logistic.dmi b/icons/obj/machines/logistic.dmi new file mode 100644 index 000000000000..6335f4cab483 Binary files /dev/null and b/icons/obj/machines/logistic.dmi differ diff --git a/taucetistation.dme b/taucetistation.dme index fa5b91d7c202..584f8949d164 100644 --- a/taucetistation.dme +++ b/taucetistation.dme @@ -488,6 +488,7 @@ #include "code\datums\wires\autholathe.dm" #include "code\datums\wires\barber.dm" #include "code\datums\wires\camera.dm" +#include "code\datums\wires\manipulator.dm" #include "code\datums\wires\mining_drill.dm" #include "code\datums\wires\mulebot.dm" #include "code\datums\wires\nuclearbomb.dm" @@ -821,6 +822,7 @@ #include "code\game\machinery\lightswitch.dm" #include "code\game\machinery\machinery.dm" #include "code\game\machinery\magnet.dm" +#include "code\game\machinery\manipulator.dm" #include "code\game\machinery\mass_driver.dm" #include "code\game\machinery\navbeacon.dm" #include "code\game\machinery\newscaster.dm"