diff --git a/code/game/objects/items/robot/ai_upgrades.dm b/code/game/objects/items/robot/ai_upgrades.dm index 26f6b553ce4f..f3b5b032231c 100644 --- a/code/game/objects/items/robot/ai_upgrades.dm +++ b/code/game/objects/items/robot/ai_upgrades.dm @@ -8,7 +8,6 @@ icon = 'icons/obj/module.dmi' icon_state = "datadisk3" - /obj/item/malf_upgrade/afterattack(mob/living/silicon/ai/AI, mob/user) . = ..() if(!istype(AI)) @@ -20,12 +19,11 @@ to_chat(AI, span_userdanger("[user] has upgraded you with combat software!")) to_chat(AI, span_userdanger("Your current laws and objectives remain unchanged.")) //this unlocks malf powers, but does not give the license to plasma flood AI.add_malf_picker() - log_game("[key_name(user)] has upgraded [key_name(AI)] with a [src].") - message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with a [src].") + log_game("[key_name(user)] has upgraded [key_name(AI)] with \a [src].") + message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with \a [src].") to_chat(user, span_notice("You upgrade [AI]. [src] is consumed in the process.")) qdel(src) - //Lipreading /obj/item/surveillance_upgrade name = "surveillance software upgrade" @@ -42,6 +40,146 @@ to_chat(AI, span_userdanger("[user] has upgraded you with surveillance software!")) to_chat(AI, "Via a combination of hidden microphones and lip reading software, you are able to use your cameras to listen in on conversations.") to_chat(user, span_notice("You upgrade [AI]. [src] is consumed in the process.")) - log_game("[key_name(user)] has upgraded [key_name(AI)] with a [src].") - message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with a [src].") + log_game("[key_name(user)] has upgraded [key_name(AI)] with \a [src].") + message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with \a [src].") qdel(src) + +/obj/item/cameragun_upgrade + name = "camera laser upgrade" + desc = "A software package that will allow an artificial intelligence to briefly increase the amount of light an camera outputs to an outrageous amount to the point it burns skins. Must be installed using an unlocked AI control console." // In short, laser gun! + icon = 'icons/obj/module.dmi' + icon_state = "datadisk3" + +/obj/item/cameragun_upgrade/afterattack(mob/living/silicon/ai/AI, mob/user) + . = ..() + if(!istype(AI)) + return + + // No giving multiple copies. + for(var/datum/action/action in AI.actions) + if(action.type == /datum/action/innate/ai/ranged/cameragun) + to_chat(user, span_notice("[AI] has already been upgraded with \a [src].")) + return + + var/datum/action/innate/ai/ranged/cameragun/ability = new + ability.Grant(AI) + + to_chat(user, span_notice("You upgrade [AI]. [src] is consumed in the process.")) + log_game("[key_name(user)] has upgraded [key_name(AI)] with \a [src].") + message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with \a [src].") + qdel(src) + +/// An ability that allows the user to shoot a laser beam at a target from the nearest camera. +/datum/action/innate/ai/ranged/cameragun + name = "Camera Laser Gun" + desc = "Shoots a laser from the nearest available camera toward a chosen destination if it is highly probable to reach said destination." + button_icon = 'icons/obj/guns/energy.dmi' + button_icon_state = "laser" + enable_text = span_notice("You prepare to overcharge a camera. Click a target for a nearby camera to shoot a laser at.") + disable_text = span_notice("You dissipate the overcharged energy.") + click_action = FALSE // Even though that we are a click action, we want to use Activate() and Deactivate(). + COOLDOWN_DECLARE(next_shot) + var/cooldown = 10 SECONDS + +/// Checks if it is possible for a (hitscan) projectile to reach a target in a straight line from a camera. +/datum/action/innate/ai/ranged/cameragun/proc/can_shoot_to(obj/machinery/camera/C, atom/target) + var/obj/projectile/proj = new /obj/projectile + proj.icon = null + proj.icon_state = null + proj.hitsound = "" + proj.suppressed = TRUE + proj.ricochets_max = 0 + proj.ricochet_chance = 0 + proj.damage = 0 + proj.nodamage = TRUE // Prevents this projectile from detonating certain objects (e.g. welding tanks). + proj.log_override = TRUE + proj.hitscan = TRUE + proj.pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE + + proj.preparePixelProjectile(target, C) + proj.fire() + + var/turf/target_turf = get_turf(target) + var/turf/last_turf = proj.hitscan_last + if(last_turf == target_turf) + return TRUE + return FALSE + +/datum/action/innate/ai/ranged/cameragun/New() + ..() + START_PROCESSING(SSfastprocess, src) + +/datum/action/innate/ai/ranged/cameragun/Destroy() + STOP_PROCESSING(SSfastprocess, src) + return ..() + +/datum/action/innate/ai/ranged/cameragun/process() + build_all_button_icons() // To update the button to display if active/available. + +/datum/action/innate/ai/ranged/cameragun/Activate(loud = TRUE) + set_ranged_ability(owner, loud ? enable_text : null) + active = TRUE + background_icon_state = "bg_default_on" + build_all_button_icons() + +/datum/action/innate/ai/ranged/cameragun/Deactivate(loud = TRUE) + unset_ranged_ability(owner, loud ? disable_text : null) + active = FALSE + background_icon_state = "bg_default" + build_all_button_icons() + +/datum/action/innate/ai/ranged/cameragun/IsAvailable(feedback = FALSE) + . = ..() + if(!. || !COOLDOWN_FINISHED(src, next_shot)) + return FALSE + +/datum/action/innate/ai/ranged/cameragun/do_ability(mob/living/caller, params, atom/target) + var/turf/loc_target = get_turf(target) + var/obj/machinery/camera/chosen_camera + for(var/obj/machinery/camera/cam in GLOB.cameranet.cameras) + if(!isturf(cam.loc)) + continue + if(cam == target) + continue + if(!cam.status || cam.emped) // Non-functional camera. + continue + var/turf/loc_camera = get_turf(cam) + if(loc_target.z != loc_camera.z) + continue + if(get_dist(cam, target) <= 1) // Pointblank shot. + chosen_camera = cam + break + if(get_dist(cam, target) > 12) + continue + if(!can_shoot_to(cam, target)) // No chance to hit. + continue + if(!chosen_camera) + chosen_camera = cam + continue + if(get_dist(chosen_camera, target) > get_dist(cam, target)) // Closest camera that can hit. + chosen_camera = cam + continue + if(!chosen_camera) + Deactivate(FALSE) + to_chat(caller, span_notice("Unable to find nearby available cameras for this target.")) + return FALSE + + COOLDOWN_START(src, next_shot, cooldown) + var/turf/loc_chosen = get_turf(chosen_camera) + var/obj/projectile/beam/laser/proj = new(loc_chosen) + proj.preparePixelProjectile(target, chosen_camera) + proj.firer = caller + + // Fire the shot. + var/pointblank = get_dist(chosen_camera, target) <= 1 ? TRUE : FALSE // Same tile or right next. + if(pointblank) + chosen_camera.visible_message(span_danger("[chosen_camera] fires a laser point blank at [target]!")) + proj.fire(direct_target = target) + else + chosen_camera.visible_message(span_danger("[chosen_camera] fires a laser!")) + proj.fire() + Deactivate(FALSE) + to_chat(caller, span_danger("Camera overcharged.")) + + chosen_camera.emp_act(EMP_HEAVY) // 90 seconds downtime -- definitely enough time to toolbox this camera (unless it is emp-proof). + return TRUE diff --git a/code/modules/mob/living/silicon/ai/decentralized/management/ai_controlpanel.dm b/code/modules/mob/living/silicon/ai/decentralized/management/ai_controlpanel.dm index eaccf21f5213..f0ac4cb859fa 100644 --- a/code/modules/mob/living/silicon/ai/decentralized/management/ai_controlpanel.dm +++ b/code/modules/mob/living/silicon/ai/decentralized/management/ai_controlpanel.dm @@ -80,7 +80,17 @@ GLOBAL_VAR_INIT(ai_control_code, random_nukecode(6)) return ..() var/obj/item/surveillance_upgrade/upgrade = W upgrade.afterattack(AI, user) - + return FALSE + if(istype(W, /obj/item/cameragun_upgrade)) + if(!authenticated) + to_chat(user, span_warning("You need to be logged in to do this!")) + return ..() + var/mob/living/silicon/ai/AI = input("Select an AI", "Select an AI", null, null) as null|anything in GLOB.ai_list + if(!AI) + return ..() + var/obj/item/cameragun_upgrade/upgrade = W + upgrade.afterattack(AI, user) + return FALSE if(istype(W, /obj/item/malf_upgrade)) if(!authenticated) to_chat(user, span_warning("You need to be logged in to do this!")) @@ -90,7 +100,7 @@ GLOBAL_VAR_INIT(ai_control_code, random_nukecode(6)) return ..() var/obj/item/malf_upgrade/upgrade = W upgrade.afterattack(AI, user) - + return FALSE return ..() /obj/machinery/computer/ai_control_console/emag_act(mob/user, obj/item/card/emag/emag_card) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index f8c23aa21917..6535c89380a5 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -58,7 +58,8 @@ var/hitscan = FALSE //Whether this is hitscan. If it is, speed is basically ignored. var/list/beam_segments //assoc list of datum/point or datum/point/vector, start = end. Used for hitscan effect generation. var/datum/point/beam_index - var/turf/hitscan_last //last turf touched during hitscanning. + /// The ending/last touched turf during hitscanning. + var/turf/hitscan_last var/tracer_type var/muzzle_type var/impact_type @@ -584,10 +585,9 @@ pixel_x = trajectory.return_px() pixel_y = trajectory.return_py() forcemoved = TRUE - hitscan_last = loc else if(T != loc) step_towards(src, T) - hitscan_last = loc + hitscan_last = T if(!hitscanning && !forcemoved) pixel_x = trajectory.return_px() - trajectory.mpx * trajectory_multiplier * SSprojectiles.global_iterations_per_move pixel_y = trajectory.return_py() - trajectory.mpy * trajectory_multiplier * SSprojectiles.global_iterations_per_move diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm index 3adb41e75d0a..0acb0926aad9 100644 --- a/code/modules/uplink/uplink_items.dm +++ b/code/modules/uplink/uplink_items.dm @@ -2680,6 +2680,13 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item)) cost = 4 // Not as "destructive" as the emag. In addition, less features than the normal emag. Increase price once more impactful features are added. restricted_roles = list("Clown") +/datum/uplink_item/role_restricted/ai_cameragun + name = "AI Camera Laser Gun Upgrade" + desc = "A disk containing experimental and illegal software that allows an AI to temporarily override the safety features on their cameras, enabling them to shoot a laser beam out of them." + item = /obj/item/cameragun_upgrade + cost = 8 // Considering that you have to: subvert an AI, trust the AI not to be a traitor/unsubverted, accept that every camera is gonna get disabled later, and pay more than an emag for this... 8 is fair. + restricted_roles = list("Roboticist", "Research Director") + // Pointless /datum/uplink_item/badass category = "(Pointless) Badassery"