|
8 | 8 | icon = 'icons/obj/module.dmi' |
9 | 9 | icon_state = "datadisk3" |
10 | 10 |
|
11 | | - |
12 | 11 | /obj/item/malf_upgrade/afterattack(mob/living/silicon/ai/AI, mob/user) |
13 | 12 | . = ..() |
14 | 13 | if(!istype(AI)) |
|
20 | 19 | to_chat(AI, span_userdanger("[user] has upgraded you with combat software!")) |
21 | 20 | 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 |
22 | 21 | AI.add_malf_picker() |
23 | | - log_game("[key_name(user)] has upgraded [key_name(AI)] with a [src].") |
24 | | - message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with a [src].") |
| 22 | + log_game("[key_name(user)] has upgraded [key_name(AI)] with \a [src].") |
| 23 | + message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with \a [src].") |
25 | 24 | to_chat(user, span_notice("You upgrade [AI]. [src] is consumed in the process.")) |
26 | 25 | qdel(src) |
27 | 26 |
|
28 | | - |
29 | 27 | //Lipreading |
30 | 28 | /obj/item/surveillance_upgrade |
31 | 29 | name = "surveillance software upgrade" |
|
42 | 40 | to_chat(AI, span_userdanger("[user] has upgraded you with surveillance software!")) |
43 | 41 | 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.") |
44 | 42 | to_chat(user, span_notice("You upgrade [AI]. [src] is consumed in the process.")) |
45 | | - log_game("[key_name(user)] has upgraded [key_name(AI)] with a [src].") |
46 | | - message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with a [src].") |
| 43 | + log_game("[key_name(user)] has upgraded [key_name(AI)] with \a [src].") |
| 44 | + message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with \a [src].") |
| 45 | + qdel(src) |
| 46 | + |
| 47 | +/obj/item/cameragun_upgrade |
| 48 | + name = "camera laser upgrade" |
| 49 | + 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! |
| 50 | + icon = 'icons/obj/module.dmi' |
| 51 | + icon_state = "datadisk3" |
| 52 | + |
| 53 | +/obj/item/cameragun_upgrade/afterattack(mob/living/silicon/ai/AI, mob/user) |
| 54 | + . = ..() |
| 55 | + if(!istype(AI)) |
| 56 | + return |
| 57 | + |
| 58 | + var/datum/action/innate/ai/ranged/cameragun/ai_action |
| 59 | + for(var/datum/action/innate/ai/ranged/cameragun/listed_action in AI.actions) |
| 60 | + if(!listed_action.from_traitor) // Duplicate. |
| 61 | + to_chat(user, span_notice("[AI] has already been upgraded with \a [src].")) |
| 62 | + return |
| 63 | + ai_action = listed_action // If they somehow have more than one action, blame adminbus first. |
| 64 | + ai_action.from_traitor = FALSE // Let them keep the action if they lose traitor status. |
| 65 | + |
| 66 | + if(!ai_action) |
| 67 | + ai_action = new |
| 68 | + ai_action.Grant(AI) |
| 69 | + |
| 70 | + to_chat(user, span_notice("You upgrade [AI]. [src] is consumed in the process.")) |
| 71 | + log_game("[key_name(user)] has upgraded [key_name(AI)] with \a [src].") |
| 72 | + message_admins("[ADMIN_LOOKUPFLW(user)] has upgraded [ADMIN_LOOKUPFLW(AI)] with \a [src].") |
47 | 73 | qdel(src) |
| 74 | + |
| 75 | +/// An ability that allows the user to shoot a laser beam at a target from the nearest camera. |
| 76 | +// TODO: If right-click functionality for buttons are added, make singleshot a left-click ability & burstmode a right-click ability. |
| 77 | +/datum/action/innate/ai/ranged/cameragun |
| 78 | + name = "Camera Laser Gun" |
| 79 | + 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." |
| 80 | + button_icon = 'icons/obj/guns/energy.dmi' |
| 81 | + button_icon_state = "laser" |
| 82 | + background_icon_state = "bg_default" // Better button sprites welcomed. :) |
| 83 | + enable_text = span_notice("You prepare to overcharge a camera. Click a target for a nearby camera to shoot a laser at.") |
| 84 | + disable_text = span_notice("You dissipate the overcharged energy.") |
| 85 | + click_action = FALSE // Even though that we are a click action, we want to use Activate() and Deactivate(). |
| 86 | + /// The beam projectile that is spawned and shot. |
| 87 | + var/obj/projectile/beam/proj_type = /obj/projectile/beam/laser |
| 88 | + /// Pass flags used for the `can_shoot_to` proc. |
| 89 | + var/proj_pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE |
| 90 | + /// If this ability is sourced from being a traitor AI. |
| 91 | + var/from_traitor = FALSE |
| 92 | + /// Is burst mode activated? |
| 93 | + var/burstmode_activated = FALSE |
| 94 | + /// How long is burst mode? |
| 95 | + var/burstmode_length = 30 SECONDS |
| 96 | + COOLDOWN_DECLARE(since_burstmode) |
| 97 | + /// How much time (after burst mode is deactivated) must pass before it can be activated again? |
| 98 | + var/activate_cooldown = 60 SECONDS |
| 99 | + COOLDOWN_DECLARE(next_activate) |
| 100 | + /// How much time between shots (during burst mode)? |
| 101 | + var/fire_cooldown = 1 SECONDS |
| 102 | + COOLDOWN_DECLARE(next_fire) |
| 103 | + /// What EMP strength will the camera be hit with after it is used to shoot? |
| 104 | + var/emp_drawback = EMP_HEAVY // 7+ guarantees a 90 seconds downtime. |
| 105 | + |
| 106 | +/// Checks if it is possible for a (hitscan) projectile to reach a target in a straight line from a camera. |
| 107 | +/datum/action/innate/ai/ranged/cameragun/proc/can_shoot_to(obj/machinery/camera/C, atom/target) |
| 108 | + var/obj/projectile/proj = new /obj/projectile |
| 109 | + proj.icon = null |
| 110 | + proj.icon_state = null |
| 111 | + proj.hitsound = "" |
| 112 | + proj.suppressed = TRUE |
| 113 | + proj.ricochets_max = 0 |
| 114 | + proj.ricochet_chance = 0 |
| 115 | + proj.damage = 0 |
| 116 | + proj.nodamage = TRUE // Prevents this projectile from detonating certain objects (e.g. welding tanks). |
| 117 | + proj.log_override = TRUE |
| 118 | + proj.hitscan = TRUE |
| 119 | + proj.pass_flags = proj_pass_flags |
| 120 | + |
| 121 | + proj.preparePixelProjectile(target, C) |
| 122 | + proj.fire() |
| 123 | + |
| 124 | + var/turf/target_turf = get_turf(target) |
| 125 | + var/turf/last_turf = proj.hitscan_last |
| 126 | + if(last_turf == target_turf) |
| 127 | + return TRUE |
| 128 | + return FALSE |
| 129 | + |
| 130 | +/datum/action/innate/ai/ranged/cameragun/New() |
| 131 | + ..() |
| 132 | + START_PROCESSING(SSfastprocess, src) |
| 133 | + |
| 134 | +/datum/action/innate/ai/ranged/cameragun/Destroy() |
| 135 | + STOP_PROCESSING(SSfastprocess, src) |
| 136 | + return ..() |
| 137 | + |
| 138 | +/datum/action/innate/ai/ranged/cameragun/process() |
| 139 | + if(burstmode_activated && COOLDOWN_FINISHED(src, since_burstmode)) |
| 140 | + toggle_burstmode() |
| 141 | + build_all_button_icons() |
| 142 | + |
| 143 | +/datum/action/innate/ai/ranged/cameragun/Activate(loud = TRUE) |
| 144 | + set_ranged_ability(owner, loud ? enable_text : null) |
| 145 | + active = TRUE |
| 146 | + background_icon_state = "bg_default_on" |
| 147 | + build_all_button_icons() |
| 148 | + |
| 149 | +/datum/action/innate/ai/ranged/cameragun/Deactivate(loud = TRUE) |
| 150 | + unset_ranged_ability(owner, loud ? disable_text : null) |
| 151 | + active = FALSE |
| 152 | + background_icon_state = "bg_default" |
| 153 | + build_all_button_icons() |
| 154 | + |
| 155 | +/datum/action/innate/ai/ranged/cameragun/IsAvailable(feedback = FALSE) |
| 156 | + . = ..() |
| 157 | + if(!.) |
| 158 | + return FALSE |
| 159 | + if(burstmode_activated && !COOLDOWN_FINISHED(src, next_fire)) // Not ready to shoot (during brustmode). |
| 160 | + return FALSE |
| 161 | + if(!burstmode_activated && !COOLDOWN_FINISHED(src, next_activate)) // Burstmode is not ready. |
| 162 | + return FALSE |
| 163 | + |
| 164 | +/datum/action/innate/ai/ranged/cameragun/do_ability(mob/living/caller, params, atom/target) |
| 165 | + var/turf/loc_target = get_turf(target) |
| 166 | + var/obj/machinery/camera/chosen_camera |
| 167 | + for(var/obj/machinery/camera/cam in GLOB.cameranet.cameras) |
| 168 | + if(!isturf(cam.loc)) |
| 169 | + continue |
| 170 | + if(cam == target) |
| 171 | + continue |
| 172 | + if(!cam.status || cam.emped) // Non-functional camera. |
| 173 | + continue |
| 174 | + var/turf/loc_camera = get_turf(cam) |
| 175 | + if(loc_target.z != loc_camera.z) |
| 176 | + continue |
| 177 | + if(get_dist(cam, target) <= 1) // Pointblank shot. |
| 178 | + chosen_camera = cam |
| 179 | + break |
| 180 | + if(get_dist(cam, target) > 12) |
| 181 | + continue |
| 182 | + if(!can_shoot_to(cam, target)) // No chance to hit. |
| 183 | + continue |
| 184 | + if(!chosen_camera) |
| 185 | + chosen_camera = cam |
| 186 | + continue |
| 187 | + if(get_dist(chosen_camera, target) > get_dist(cam, target)) // Closest camera that can hit. |
| 188 | + chosen_camera = cam |
| 189 | + continue |
| 190 | + if(!chosen_camera) |
| 191 | + Deactivate(FALSE) |
| 192 | + to_chat(caller, span_notice("Unable to find nearby available cameras for this target.")) |
| 193 | + return FALSE |
| 194 | + if(!burstmode_activated) |
| 195 | + toggle_burstmode() |
| 196 | + |
| 197 | + COOLDOWN_START(src, next_fire, fire_cooldown) |
| 198 | + var/turf/loc_chosen = get_turf(chosen_camera) |
| 199 | + var/obj/projectile/beam/proj = new proj_type(loc_chosen) |
| 200 | + if(!isprojectile(proj)) |
| 201 | + Deactivate(FALSE) |
| 202 | + CRASH("Camera gun's proj_type was not a projectile.") |
| 203 | + proj.preparePixelProjectile(target, chosen_camera) |
| 204 | + proj.firer = caller |
| 205 | + |
| 206 | + // Fire the shot. |
| 207 | + var/pointblank = get_dist(chosen_camera, target) <= 1 ? TRUE : FALSE // Same tile or right next. |
| 208 | + if(pointblank) |
| 209 | + chosen_camera.visible_message(span_danger("[chosen_camera] fires a laser point blank at [target]!")) |
| 210 | + proj.fire(direct_target = target) |
| 211 | + else |
| 212 | + chosen_camera.visible_message(span_danger("[chosen_camera] fires a laser!")) |
| 213 | + proj.fire() |
| 214 | + Deactivate(FALSE) |
| 215 | + to_chat(caller, span_danger("Camera overcharged.")) |
| 216 | + |
| 217 | + /* This EMP prevents burstmode from annihilating a stationary object/person. |
| 218 | + If someone gives a camera EMP resistance, then they had it coming. */ |
| 219 | + if(emp_drawback > 0) |
| 220 | + chosen_camera.emp_act(emp_drawback) |
| 221 | + return TRUE |
| 222 | + |
| 223 | +/datum/action/innate/ai/ranged/cameragun/proc/toggle_burstmode() |
| 224 | + burstmode_activated = !burstmode_activated |
| 225 | + if(burstmode_activated) |
| 226 | + COOLDOWN_START(src, since_burstmode, burstmode_length) |
| 227 | + to_chat(owner, span_notice("Burstmode activated.")) |
| 228 | + owner.playsound_local(owner, 'sound/effects/light_flicker.ogg', 50, FALSE) |
| 229 | + else |
| 230 | + COOLDOWN_START(src, next_activate, activate_cooldown) |
| 231 | + to_chat(owner, span_notice("Burstmode deactivated.")) |
| 232 | + Deactivate(FALSE) // In case that they were in the middle of shooting. |
| 233 | + owner.playsound_local(owner, 'sound/items/timer.ogg', 50, FALSE) |
| 234 | + return TRUE |
0 commit comments