Skip to content

Commit

Permalink
Traitor AI's 20 Nonexistent TCs just for an AI Camera Laser Gun (#21118)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Runian authored Mar 8, 2024
1 parent 7dc11ec commit fa79635
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 11 deletions.
199 changes: 193 additions & 6 deletions code/game/objects/items/robot/ai_upgrades.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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"
Expand All @@ -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
13 changes: 13 additions & 0 deletions code/modules/antagonists/traitor/datum_traitor.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!"))
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions code/modules/projectiles/projectile.dm
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,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
Expand Down Expand Up @@ -604,10 +605,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
Expand Down

0 comments on commit fa79635

Please sign in to comment.