Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Commit fa79635

Browse files
authored
Traitor AI's 20 Nonexistent TCs just for an AI Camera Laser Gun (#21118)
* the action and the item * uplink item and cost reason * no need for msg due to isavailable * better var name * accuracy (almost) guaranteed!!! * now can target objects (but not specific turfs) * better comment * better uplink name * EMP_HEAVY * proper logging msg * no dupes + fixing other upgrades logging * dont need to null it i think * increases pointblank from 0tile -> 1tile * unused parameter * yeet this out of uplink items * traitor ai got it now * no traitor dupes. also dunno how to get all specific types in a list and i know this works sooooooooooo * want this background icon state immediately * i enknowledge the button sprite suck ass * there * cooldown adjustments * comment explictly saying that burstmode is good for emp resist cameras * makes it easy to adminbus projs or add subtypes * custom emp drawback * edge case * the difference only matters in no lag environment
1 parent 7dc11ec commit fa79635

4 files changed

Lines changed: 221 additions & 11 deletions

File tree

code/game/objects/items/robot/ai_upgrades.dm

Lines changed: 193 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
icon = 'icons/obj/module.dmi'
99
icon_state = "datadisk3"
1010

11-
1211
/obj/item/malf_upgrade/afterattack(mob/living/silicon/ai/AI, mob/user)
1312
. = ..()
1413
if(!istype(AI))
@@ -20,12 +19,11 @@
2019
to_chat(AI, span_userdanger("[user] has upgraded you with combat software!"))
2120
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
2221
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].")
2524
to_chat(user, span_notice("You upgrade [AI]. [src] is consumed in the process."))
2625
qdel(src)
2726

28-
2927
//Lipreading
3028
/obj/item/surveillance_upgrade
3129
name = "surveillance software upgrade"
@@ -42,6 +40,195 @@
4240
to_chat(AI, span_userdanger("[user] has upgraded you with surveillance software!"))
4341
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.")
4442
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].")
4773
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

code/modules/antagonists/traitor/datum_traitor.dm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
if(traitor_kind == TRAITOR_AI && owner.current && isAI(owner.current))
6363
var/mob/living/silicon/ai/A = owner.current
6464
A.set_zeroth_law("")
65+
for(var/datum/action/innate/ai/ranged/cameragun/ai_action in A.actions)
66+
if(ai_action.from_traitor)
67+
ai_action.Remove(A)
6568
if(malf)
6669
remove_verb(A, /mob/living/silicon/ai/proc/choose_modules)
6770
A.malf_picker.remove_malf_verbs(A)
@@ -261,6 +264,16 @@
261264
add_law_zero()
262265
owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/malf.ogg', 100, FALSE, pressure_affected = FALSE)
263266
owner.current.grant_language(/datum/language/codespeak, TRUE, TRUE, LANGUAGE_MALF)
267+
268+
var/has_action = FALSE
269+
for(var/datum/action/innate/ai/ranged/cameragun/ai_action in owner.current.actions)
270+
has_action = TRUE
271+
break
272+
if(!has_action)
273+
var/datum/action/innate/ai/ranged/cameragun/ability = new
274+
ability.from_traitor = TRUE
275+
ability.Grant(owner.current)
276+
264277
if(TRAITOR_HUMAN)
265278
if(should_equip)
266279
equip(silent)

code/modules/mob/living/silicon/ai/decentralized/management/ai_controlpanel.dm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,17 @@ GLOBAL_VAR_INIT(ai_control_code, random_nukecode(6))
8080
return ..()
8181
var/obj/item/surveillance_upgrade/upgrade = W
8282
upgrade.afterattack(AI, user)
83-
83+
return FALSE
84+
if(istype(W, /obj/item/cameragun_upgrade))
85+
if(!authenticated)
86+
to_chat(user, span_warning("You need to be logged in to do this!"))
87+
return ..()
88+
var/mob/living/silicon/ai/AI = input("Select an AI", "Select an AI", null, null) as null|anything in GLOB.ai_list
89+
if(!AI)
90+
return ..()
91+
var/obj/item/cameragun_upgrade/upgrade = W
92+
upgrade.afterattack(AI, user)
93+
return FALSE
8494
if(istype(W, /obj/item/malf_upgrade))
8595
if(!authenticated)
8696
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))
90100
return ..()
91101
var/obj/item/malf_upgrade/upgrade = W
92102
upgrade.afterattack(AI, user)
93-
103+
return FALSE
94104
return ..()
95105

96106
/obj/machinery/computer/ai_control_console/emag_act(mob/user, obj/item/card/emag/emag_card)

code/modules/projectiles/projectile.dm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
var/hitscan = FALSE //Whether this is hitscan. If it is, speed is basically ignored.
6868
var/list/beam_segments //assoc list of datum/point or datum/point/vector, start = end. Used for hitscan effect generation.
6969
var/datum/point/beam_index
70-
var/turf/hitscan_last //last turf touched during hitscanning.
70+
/// The ending/last touched turf during hitscanning.
71+
var/turf/hitscan_last
7172
var/tracer_type
7273
var/muzzle_type
7374
var/impact_type
@@ -604,10 +605,9 @@
604605
pixel_x = trajectory.return_px()
605606
pixel_y = trajectory.return_py()
606607
forcemoved = TRUE
607-
hitscan_last = loc
608608
else if(T != loc)
609609
step_towards(src, T)
610-
hitscan_last = loc
610+
hitscan_last = T
611611
if(!hitscanning && !forcemoved)
612612
pixel_x = trajectory.return_px() - trajectory.mpx * trajectory_multiplier * SSprojectiles.global_iterations_per_move
613613
pixel_y = trajectory.return_py() - trajectory.mpy * trajectory_multiplier * SSprojectiles.global_iterations_per_move

0 commit comments

Comments
 (0)