diff --git a/code/game/objects/items/robot/ai_upgrades.dm b/code/game/objects/items/robot/ai_upgrades.dm index 26f6b553ce4f..97498bd0b8a4 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,195 @@ 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 + + var/datum/action/innate/ai/ranged/cameragun/ai_action + for(var/datum/action/innate/ai/ranged/cameragun/listed_action in AI.actions) + if(!listed_action.from_traitor) // Duplicate. + to_chat(user, span_notice("[AI] has already been upgraded with \a [src].")) + return + ai_action = listed_action // If they somehow have more than one action, blame adminbus first. + ai_action.from_traitor = FALSE // Let them keep the action if they lose traitor status. + + if(!ai_action) + ai_action = new + ai_action.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. +// TODO: If right-click functionality for buttons are added, make singleshot a left-click ability & burstmode a right-click ability. +/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. If successful, enters burst mode which temporarily allows the ability to be reused every second for 30 seconds." + button_icon = 'icons/obj/guns/energy.dmi' + button_icon_state = "laser" + background_icon_state = "bg_default" // Better button sprites welcomed. :) + 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(). + /// The beam projectile that is spawned and shot. + var/obj/projectile/beam/proj_type = /obj/projectile/beam/laser + /// Pass flags used for the `can_shoot_to` proc. + var/proj_pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE + /// If this ability is sourced from being a traitor AI. + var/from_traitor = FALSE + /// Is burst mode activated? + var/burstmode_activated = FALSE + /// How long is burst mode? + var/burstmode_length = 30 SECONDS + COOLDOWN_DECLARE(since_burstmode) + /// How much time (after burst mode is deactivated) must pass before it can be activated again? + var/activate_cooldown = 60 SECONDS + COOLDOWN_DECLARE(next_activate) + /// How much time between shots (during burst mode)? + var/fire_cooldown = 1 SECONDS + COOLDOWN_DECLARE(next_fire) + /// What EMP strength will the camera be hit with after it is used to shoot? + var/emp_drawback = EMP_HEAVY // 7+ guarantees a 90 seconds downtime. + +/// 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 = proj_pass_flags + + 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() + if(burstmode_activated && COOLDOWN_FINISHED(src, since_burstmode)) + toggle_burstmode() + build_all_button_icons() + +/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(!.) + return FALSE + if(burstmode_activated && !COOLDOWN_FINISHED(src, next_fire)) // Not ready to shoot (during brustmode). + return FALSE + if(!burstmode_activated && !COOLDOWN_FINISHED(src, next_activate)) // Burstmode is not ready. + 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 + if(!burstmode_activated) + toggle_burstmode() + + COOLDOWN_START(src, next_fire, fire_cooldown) + var/turf/loc_chosen = get_turf(chosen_camera) + var/obj/projectile/beam/proj = new proj_type(loc_chosen) + if(!isprojectile(proj)) + Deactivate(FALSE) + CRASH("Camera gun's proj_type was not a projectile.") + 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.")) + + /* This EMP prevents burstmode from annihilating a stationary object/person. + If someone gives a camera EMP resistance, then they had it coming. */ + if(emp_drawback > 0) + chosen_camera.emp_act(emp_drawback) + return TRUE + +/datum/action/innate/ai/ranged/cameragun/proc/toggle_burstmode() + burstmode_activated = !burstmode_activated + if(burstmode_activated) + COOLDOWN_START(src, since_burstmode, burstmode_length) + to_chat(owner, span_notice("Burstmode activated.")) + owner.playsound_local(owner, 'sound/effects/light_flicker.ogg', 50, FALSE) + else + COOLDOWN_START(src, next_activate, activate_cooldown) + to_chat(owner, span_notice("Burstmode deactivated.")) + Deactivate(FALSE) // In case that they were in the middle of shooting. + owner.playsound_local(owner, 'sound/items/timer.ogg', 50, FALSE) + return TRUE diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 5f5e81ec0750..e6a6c3ff4250 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -62,6 +62,9 @@ if(traitor_kind == TRAITOR_AI && owner.current && isAI(owner.current)) var/mob/living/silicon/ai/A = owner.current A.set_zeroth_law("") + for(var/datum/action/innate/ai/ranged/cameragun/ai_action in A.actions) + if(ai_action.from_traitor) + ai_action.Remove(A) if(malf) remove_verb(A, /mob/living/silicon/ai/proc/choose_modules) A.malf_picker.remove_malf_verbs(A) @@ -261,6 +264,16 @@ add_law_zero() owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/malf.ogg', 100, FALSE, pressure_affected = FALSE) owner.current.grant_language(/datum/language/codespeak, TRUE, TRUE, LANGUAGE_MALF) + + var/has_action = FALSE + for(var/datum/action/innate/ai/ranged/cameragun/ai_action in owner.current.actions) + has_action = TRUE + break + if(!has_action) + var/datum/action/innate/ai/ranged/cameragun/ability = new + ability.from_traitor = TRUE + ability.Grant(owner.current) + if(TRAITOR_HUMAN) if(should_equip) equip(silent) 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 9ff8d1a66c91..2f8a3c2f8b0a 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 e94332a20b9a..a66f08bc62b3 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -57,7 +57,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 @@ -586,10 +587,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