diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm
index 5796515abf8d..aa9c85658c30 100644
--- a/code/__DEFINES/ai.dm
+++ b/code/__DEFINES/ai.dm
@@ -21,13 +21,19 @@
//AI Project Categories.
#define AI_PROJECT_HUDS "Sensor HUDs"
#define AI_PROJECT_CAMERAS "Visiblity Upgrades"
+#define AI_PROJECT_INDUCTION "Induction"
+#define AI_PROJECT_SURVEILLANCE "Surveillance"
#define AI_PROJECT_MISC "Misc."
//Update this list if you add any new ones, else the category won't show up in the UIs
GLOBAL_LIST_INIT(ai_project_categories, list(
AI_PROJECT_HUDS,
AI_PROJECT_CAMERAS,
+ AI_PROJECT_SURVEILLANCE,
+ AI_PROJECT_INDUCTION,
AI_PROJECT_MISC
))
///How much is the AI download progress increased by per tick? Multiplied by a modifer on the AI if they have upgraded. Need to reach 100 to be downloaded
#define AI_DOWNLOAD_PER_PROCESS 0.75
+///Check for tracked individual coming into view every X ticks
+#define AI_CAMERA_MEMORY_TICKS 15
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index 34303a2907b9..b988460fe68f 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -182,12 +182,21 @@
hangup_all_calls()
add_hiddenprint(usr)
-/* Humans (With upgrade) */
+/* Humans (With upgrades) */
/mob/living/carbon/human/AIShiftClick(mob/living/silicon/ai/user)
- if(!user.canExamineHumans)
- return
+
if(user.client && (user.client.eye == user.eyeobj || user.client.eye == user.loc))
- user.examinate(src)
+ if(user.canExamineHumans)
+ user.examinate(src)
+ if(user.canCameraMemoryTrack)
+ if(name == "Unknown")
+ to_chat(user, span_warning("Unable to track 'Unknown' persons! Their name must be visible."))
+ return
+ if(src == user.cameraMemoryTarget)
+ to_chat(user, span_warning("Stop tracking this individual? \[UNTRACK\]"))
+ else
+ to_chat(user, span_warning("Track this individual? \[TRACK\]"))
+ return
//
// Override TurfAdjacent for AltClicking
diff --git a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm
index 5183b1778834..7b0c5e993372 100644
--- a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm
+++ b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm
@@ -28,10 +28,20 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/AI_Module))
var/mob/living/silicon/ai/owner_AI
/// If we have multiple uses of the same power
var/uses
+ ///How many uses can we store up? Only used for non-antag AI upgrade
+ var/max_uses
+ ///delete the ability when we're out of uses?
+ var/delete_on_empty = TRUE
/// If we automatically use up uses on each activation
var/auto_use_uses = TRUE
/// If applicable, the time in deciseconds we have to wait before using any more modules
var/cooldown_period
+ //Can our uses be recharged using CPU in the reworked AI system?
+ var/can_be_recharged = FALSE
+
+/datum/action/innate/ai/New()
+ . = ..()
+ max_uses = uses
/datum/action/innate/ai/Grant(mob/living/L)
. = ..()
@@ -47,6 +57,9 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/AI_Module))
return
/datum/action/innate/ai/Trigger()
+ if(uses <= 0 && !isnull(uses))
+ to_chat(owner, span_warning("[name] has no more uses! Charge it using CPU cycles in your dashboard."))
+ return FALSE
. = ..()
if(auto_use_uses)
adjust_uses(-1)
@@ -58,9 +71,10 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/AI_Module))
if(!silent && uses)
to_chat(owner, span_notice("[name] now has [uses] use[uses > 1 ? "s" : ""] remaining."))
if(!uses)
- if(initial(uses) > 1) //no need to tell 'em if it was one-use anyway!
+ if(initial(uses) > 1 || !delete_on_empty) //no need to tell 'em if it was one-use anyway!
to_chat(owner, span_warning("[name] has run out of uses!"))
- qdel(src)
+ if(delete_on_empty)
+ qdel(src)
/// Framework for ranged abilities that can have different effects by left-clicking stuff.
/datum/action/innate/ai/ranged
@@ -85,10 +99,11 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/AI_Module))
if(!silent && uses)
to_chat(owner, span_notice("[name] now has [uses] use[uses > 1 ? "s" : ""] remaining."))
if(!uses)
- if(initial(uses) > 1) //no need to tell 'em if it was one-use anyway!
+ if(initial(uses) > 1 || !delete_on_empty) //no need to tell 'em if it was one-use anyway!
to_chat(owner, span_warning("[name] has run out of uses!"))
- Remove(owner)
- QDEL_IN(src, 100) //let any active timers on us finish up
+ if(delete_on_empty)
+ Remove(owner)
+ QDEL_IN(src, 100) //let any active timers on us finish up
/datum/action/innate/ai/ranged/Destroy()
QDEL_NULL(linked_ability)
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index f2edc44f4ae1..4040ab0c3c80 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -112,10 +112,19 @@
var/downloadSpeedModifier = 1
var/login_warned_temp = FALSE
+
+ //Do we have access to camera tracking?
+ var/canCameraMemoryTrack = FALSE
+ //The person we are tracking
+ var/cameraMemoryTarget = null
+ //We only check every X ticks
+ var/cameraMemoryTickCount = 0
+
//Did we get the death prompt?
var/is_dying = FALSE
+
/mob/living/silicon/ai/Initialize(mapload, datum/ai_laws/L, mob/target_ai, shunted)
. = ..()
if(!target_ai) //If there is no player/brain inside.
@@ -248,7 +257,14 @@
M.update()
-
+/mob/living/silicon/ai/proc/add_verb_ai(addedVerb)
+ add_verb(src, addedVerb)
+ if(istype(loc, /obj/machinery/ai/data_core)) //A BYOND bug requires you to be viewing your core before your verbs update
+ var/obj/machinery/ai/data_core/core = loc
+ forceMove(get_turf(loc))
+ view_core()
+ sleep(1)
+ forceMove(core)
/mob/living/silicon/ai/verb/pick_icon()
set category = "AI Commands"
@@ -504,6 +520,25 @@
return
if(M)
M.transfer_ai(AI_MECH_HACK, src, usr) //Called om the mech itself.
+
+ if(href_list["stopTrackHuman"])
+ if(!cameraMemoryTarget)
+ return
+ to_chat(src, span_notice("Target no longer being tracked."))
+ cameraMemoryTarget = null
+
+ if(href_list["trackHuman"])
+ var/track_name = href_list["trackHuman"]
+ if(!track_name)
+ to_chat(src, span_warning("Unable to track target."))
+ return
+ if(cameraMemoryTarget)
+ to_chat(src, span_warning("Old target discarded. Exclusively tracking new target."))
+ else
+ to_chat(src, span_notice("Now tracking new target, [track_name]."))
+
+ cameraMemoryTarget = track_name
+ cameraMemoryTickCount = 0
if(href_list["instant_download"])
if(!href_list["console"])
@@ -931,15 +966,8 @@
to_chat(src, "You are also capable of hacking APCs, which grants you more points to spend on your Malfunction powers. The drawback is that a hacked APC will give you away if spotted by the crew. Hacking an APC takes 30 seconds.")
view_core() //A BYOND bug requires you to be viewing your core before your verbs update
- add_verb(src, /mob/living/silicon/ai/proc/choose_modules)
- add_verb(src, /mob/living/silicon/ai/proc/toggle_download)
+ add_verb_ai(list(/mob/living/silicon/ai/proc/choose_modules, /mob/living/silicon/ai/proc/toggle_download))
malf_picker = new /datum/module_picker
- if(istype(loc, /obj/machinery/ai/data_core)) //A BYOND bug requires you to be viewing your core before your verbs update
- var/obj/machinery/ai/data_core/core = loc
- forceMove(get_turf(loc))
- view_core()
- sleep(1)
- forceMove(core)
/mob/living/silicon/ai/reset_perspective(atom/A)
diff --git a/code/modules/mob/living/silicon/ai/decentralized/management/ai_dashboard.dm b/code/modules/mob/living/silicon/ai/decentralized/management/ai_dashboard.dm
index c1a4a9520480..f8b06ed14cca 100644
--- a/code/modules/mob/living/silicon/ai/decentralized/management/ai_dashboard.dm
+++ b/code/modules/mob/living/silicon/ai/decentralized/management/ai_dashboard.dm
@@ -84,12 +84,21 @@
for(var/datum/ai_project/AP as anything in available_projects)
data["available_projects"] += list(list("name" = AP.name, "description" = AP.description, "ram_required" = AP.ram_required, "available" = AP.canResearch(), "research_cost" = AP.research_cost, "research_progress" = AP.research_progress,
- "assigned_cpu" = cpu_usage[AP.name] ? cpu_usage[AP.name] : 0, "research_requirements" = AP.research_requirements, "category" = AP.category))
-
+ "assigned_cpu" = cpu_usage[AP.name] ? cpu_usage[AP.name] : 0, "research_requirements" = AP.research_requirements_text, "category" = AP.category))
+ var/list/ability_paths = list()
data["completed_projects"] = list()
+ data["chargeable_abilities"] = list()
for(var/datum/ai_project/P as anything in completed_projects)
- data["completed_projects"] += list(list("name" = P.name, "description" = P.description, "ram_required" = P.ram_required, "running" = P.running, "category" = P.category))
+ data["completed_projects"] += list(list("name" = P.name, "description" = P.description, "ram_required" = P.ram_required, "running" = P.running, "category" = P.category, "can_be_run" = P.can_be_run))
+ if(P.ability_path && !(P.ability_path in ability_paths)) //Check that we've not already added a thing to recharge this type of ability
+ if(P.ability_recharge_cost <= 0)
+ continue
+ ability_paths += P.ability_path
+ var/datum/action/innate/ai/the_ability = locate(P.ability_path) in owner.actions
+ if(the_ability)
+ data["chargeable_abilities"] += list(list("assigned_cpu" = cpu_usage[P.name],"cost" = P.ability_recharge_cost, "progress" = P.ability_recharge_invested, "name" = the_ability.name,
+ "project_name" = P.name, "uses" = the_ability.uses, "max_uses" = the_ability.max_uses))
return data
@@ -114,11 +123,23 @@
to_chat(owner, span_notice("Instance of [params["project_name"]] succesfully ended."))
. = TRUE
if("allocate_cpu")
- var/datum/ai_project/project = get_project_by_name(params["project_name"])
-
+ var/datum/ai_project/project = get_project_by_name(params["project_name"], TRUE)
if(!project || !set_project_cpu(project, text2num(params["amount"])))
to_chat(owner, span_warning("Unable to add CPU to [params["project_name"]]. Either not enough free CPU or project is unavailable."))
. = TRUE
+ if("allocate_recharge_cpu")
+ var/datum/ai_project/project = get_project_by_name(params["project_name"])
+ if(!has_completed_project(project.type))
+ return
+ var/datum/action/innate/ai/the_ability = locate(project.ability_path) in owner.actions
+ if(!the_ability)
+ return
+ if(the_ability.uses >= the_ability.max_uses)
+ to_chat(owner, span_warning("This action already has the maximum amount of charges!"))
+ return
+ if(!project || !set_project_cpu(project, text2num(params["amount"])))
+ to_chat(owner, span_warning("Unable to add CPU to [params["project_name"]]. Either not enough free CPU or ability recharge is unavailable."))
+ . = TRUE
/datum/ai_dashboard/proc/get_project_by_name(project_name, only_available = FALSE)
for(var/datum/ai_project/AP as anything in available_projects)
@@ -138,6 +159,17 @@
if(amount < 0)
return FALSE
+
+ if(has_completed_project(project.type) && !project.ability_recharge_cost)
+ if(!project.ability_recharge_cost)
+ return
+ var/datum/action/innate/ai/the_ability = locate(project.ability_path) in owner.actions
+ if(!the_ability)
+ return
+ if(the_ability.uses >= the_ability.max_uses)
+ return
+
+
var/total_cpu_used = 0
for(var/I in cpu_usage)
@@ -173,20 +205,33 @@
return FALSE
-/datum/ai_dashboard/proc/has_completed_projects(project_name)
+/datum/ai_dashboard/proc/has_completed_project(project_type)
for(var/datum/ai_project/P as anything in completed_projects)
- if(P.name == project_name)
+ if(P.type == project_type)
return TRUE
return FALSE
-
/datum/ai_dashboard/proc/finish_project(datum/ai_project/project, notify_user = TRUE)
available_projects -= project
completed_projects += project
cpu_usage[project.name] = 0
+ project.finish()
if(notify_user)
to_chat(owner, span_notice("[project] has been completed. User input required."))
+/datum/ai_dashboard/proc/recharge_ability(datum/ai_project/project, notify_user = TRUE)
+ cpu_usage[project.name] = 0
+ if(!project.ability_path)
+ return
+ var/datum/action/innate/ai/ability = locate(project.ability_path) in owner.actions
+ if(!ability)
+ return
+ ability.uses++
+ project.ability_recharge_invested = 0
+
+ if(notify_user)
+ to_chat(owner, span_notice("'[ability.name]' has been recharged."))
+
//Stuff is handled in here per tick :)
/datum/ai_dashboard/proc/tick(seconds)
@@ -235,15 +280,25 @@
if(reduction_of_resources)
to_chat(owner, span_warning("Lack of computational capacity. Some programs may have been stopped."))
+
for(var/project_being_researched in cpu_usage)
if(!cpu_usage[project_being_researched])
continue
+
var/used_cpu = round(cpu_usage[project_being_researched] * seconds, 1)
- var/datum/ai_project/project = get_project_by_name(project_being_researched, TRUE)
+ var/datum/ai_project/project = get_project_by_name(project_being_researched)
if(!project)
cpu_usage[project_being_researched] = 0
continue
+ if(has_completed_project(project.type)) //This means we're an ability recharging
+ project.ability_recharge_invested += used_cpu
+ if(project.ability_recharge_invested > project.ability_recharge_cost)
+ owner.playsound_local(owner, 'sound/machines/ping.ogg', 50, 0)
+ recharge_ability(project)
+ continue
+
project.research_progress += used_cpu
if(project.research_progress > project.research_cost)
+ owner.playsound_local(owner, 'sound/machines/ping.ogg', 50, 0)
finish_project(project)
diff --git a/code/modules/mob/living/silicon/ai/decentralized/projects/_ai_project.dm b/code/modules/mob/living/silicon/ai/decentralized/projects/_ai_project.dm
index 842a91b932aa..19a53c9e798b 100644
--- a/code/modules/mob/living/silicon/ai/decentralized/projects/_ai_project.dm
+++ b/code/modules/mob/living/silicon/ai/decentralized/projects/_ai_project.dm
@@ -11,7 +11,20 @@ GLOBAL_LIST_EMPTY(ai_projects)
var/ram_required = 0
var/running = FALSE
//Text for canResearch()
- var/research_requirements = "None"
+ var/research_requirements_text = "None"
+ //list of typepaths of required projects
+ var/research_requirements
+
+ //Passive upgrades and abilities below
+
+ ///Should we be able to even run this program?
+ var/can_be_run = TRUE
+ ///Path to our ability if we have any
+ var/ability_path = FALSE
+ ///If we have an ability how many CPU cycles do they take to charge?
+ var/ability_recharge_cost = 0
+ ///How much CPU have we invested in charging it up?
+ var/ability_recharge_invested = 0
var/mob/living/silicon/ai/ai
var/datum/ai_dashboard/dashboard
@@ -24,10 +37,17 @@ GLOBAL_LIST_EMPTY(ai_projects)
..()
/datum/ai_project/proc/canResearch()
+ if(!research_requirements)
+ return TRUE
+ for(var/P in research_requirements)
+ if(!dashboard.has_completed_project(P))
+ return FALSE
return TRUE
/datum/ai_project/proc/run_project(force_run = FALSE)
SHOULD_CALL_PARENT(TRUE)
+ if(!can_be_run)
+ return FALSE
if(!force_run)
if(!canRun())
return FALSE
@@ -44,3 +64,16 @@ GLOBAL_LIST_EMPTY(ai_projects)
/datum/ai_project/proc/canRun()
SHOULD_CALL_PARENT(TRUE)
return !running
+
+//Run when project is finished. For passive upgrades or adding abilities.
+/datum/ai_project/proc/finish()
+ return
+
+/datum/ai_project/proc/add_ability(datum/action/innate/ai/ability)
+ var/datum/action/innate/ai/has_ability = locate(ability) in ai.actions
+ if(has_ability)
+ return FALSE
+
+ var/datum/action/AC = new ability()
+ AC.Grant(ai)
+ return AC
diff --git a/code/modules/mob/living/silicon/ai/decentralized/projects/ai_huds.dm b/code/modules/mob/living/silicon/ai/decentralized/projects/ai_huds.dm
index 5db9ed5ff47c..fcf9d353a458 100644
--- a/code/modules/mob/living/silicon/ai/decentralized/projects/ai_huds.dm
+++ b/code/modules/mob/living/silicon/ai/decentralized/projects/ai_huds.dm
@@ -3,7 +3,7 @@
description = "Using experimental long range passive sensors should allow you to detect various implants such as loyalty implants and tracking implants."
research_cost = 1000
ram_required = 2
- research_requirements = "None"
+ research_requirements_text = "None"
category = AI_PROJECT_HUDS
/datum/ai_project/security_hud/run_project(force_run = FALSE)
@@ -31,7 +31,7 @@
description = "Various data processing optimizations should allow you to gain extra knowledge about users when your medical and diagnostic hud is active."
research_cost = 750
ram_required = 1
- research_requirements = "None"
+ research_requirements_text = "None"
category = AI_PROJECT_HUDS
/datum/ai_project/diag_med_hud/run_project(force_run = FALSE)
diff --git a/code/modules/mob/living/silicon/ai/decentralized/projects/camera_mobility.dm b/code/modules/mob/living/silicon/ai/decentralized/projects/camera_mobility.dm
index cc11777134b9..acf11e390240 100644
--- a/code/modules/mob/living/silicon/ai/decentralized/projects/camera_mobility.dm
+++ b/code/modules/mob/living/silicon/ai/decentralized/projects/camera_mobility.dm
@@ -3,7 +3,7 @@
description = "Using advanced deep learning algorithms you could boost your camera traverse speed."
research_cost = 500
ram_required = 1
- research_requirements = "None"
+ research_requirements_text = "None"
category = AI_PROJECT_CAMERAS
/datum/ai_project/camera_speed/run_project(force_run = FALSE)
diff --git a/code/modules/mob/living/silicon/ai/decentralized/projects/examine.dm b/code/modules/mob/living/silicon/ai/decentralized/projects/examine.dm
index f04fb24be73c..c947fadc08c0 100644
--- a/code/modules/mob/living/silicon/ai/decentralized/projects/examine.dm
+++ b/code/modules/mob/living/silicon/ai/decentralized/projects/examine.dm
@@ -5,12 +5,9 @@
description = "Using experimental image enhancing algorithms will allow you to examine humans, albeit you won't be able to point out every detail.."
research_cost = 2500
ram_required = 3
- research_requirements = "Advanced Security HUD & Advanced Medical & Diagnostic HUD"
- category = AI_PROJECT_CAMERAS
-
-
-/datum/ai_project/examine_humans/canResearch()
- return (dashboard.has_completed_projects("Advanced Security HUD") && dashboard.has_completed_projects("Advanced Medical & Diagnostic HUD"))
+ research_requirements_text = "Advanced Security HUD & Advanced Medical & Diagnostic HUD"
+ research_requirements = list(/datum/ai_project/security_hud, /datum/ai_project/diag_med_hud)
+ category = AI_PROJECT_SURVEILLANCE
/datum/ai_project/examine_humans/run_project(force_run = FALSE)
. = ..(force_run)
diff --git a/code/modules/mob/living/silicon/ai/decentralized/projects/firewall.dm b/code/modules/mob/living/silicon/ai/decentralized/projects/firewall.dm
index 36b06883738c..9186e9b71e53 100644
--- a/code/modules/mob/living/silicon/ai/decentralized/projects/firewall.dm
+++ b/code/modules/mob/living/silicon/ai/decentralized/projects/firewall.dm
@@ -3,7 +3,7 @@
description = "By hiding your various functions you should be able to prolong the time it takes to download your consciousness by 2x."
research_cost = 1000
ram_required = 2
- research_requirements = "None"
+ research_requirements_text = "None"
category = AI_PROJECT_MISC
/datum/ai_project/firewall/run_project(force_run = FALSE)
diff --git a/code/modules/mob/living/silicon/ai/decentralized/projects/induction.dm b/code/modules/mob/living/silicon/ai/decentralized/projects/induction.dm
new file mode 100644
index 000000000000..3698939aa938
--- /dev/null
+++ b/code/modules/mob/living/silicon/ai/decentralized/projects/induction.dm
@@ -0,0 +1,132 @@
+/datum/ai_project/induction_basic
+ name = "Bluespace Induction Basics"
+ description = "This research functions as a prerequisite for other induction research such as remote borg charging and APC emergency power."
+ research_cost = 3000
+ ram_required = 0
+ research_requirements_text = "None"
+ can_be_run = FALSE
+ category = AI_PROJECT_INDUCTION
+
+/datum/ai_project/induction_cyborg
+ name = "Bluespace Induction - Cyborgs"
+ description = "This ability will allow you to charge any visible cyborgs by 33%"
+ research_cost = 3000
+ ram_required = 0
+ research_requirements_text = "Bluespace Induction Basics"
+ research_requirements = list(/datum/ai_project/induction_basic)
+ category = AI_PROJECT_INDUCTION
+
+ can_be_run = FALSE
+ ability_path = /datum/action/innate/ai/ranged/charge_borg_or_apc
+ ability_recharge_cost = 1500
+
+/datum/ai_project/induction_cyborg/finish()
+ var/datum/action/innate/ai/ranged/charge_borg_or_apc/ability = add_ability(/datum/action/innate/ai/ranged/charge_borg_or_apc)
+ var/obj/effect/proc_holder/ranged_ai/charge_borg_or_apc/effect = ability.linked_ability
+ if(ability)
+ effect.works_on_borgs = TRUE
+ effect.attached_action.button.name = "Charge cyborg"
+ effect.attached_action.button.desc = "Click a cyborg to charge it by 33%"
+ else
+ ability = locate(/datum/action/innate/ai/ranged/charge_borg_or_apc) in ai.actions
+ effect = ability.linked_ability
+ effect.works_on_borgs = TRUE
+ effect.attached_action.button.name = "Charge cyborg/APC"
+ effect.attached_action.button.desc = "Click a cyborg or APC to charge it by 33%"
+
+
+/datum/ai_project/induction_apc
+ name = "Bluespace Induction - APCs"
+ description = "This ability will allow you to charge any visible APCs by 33%"
+ research_cost = 3000
+ ram_required = 0
+ research_requirements_text = "Bluespace Induction Basics"
+ research_requirements = list(/datum/ai_project/induction_basic)
+ category = AI_PROJECT_INDUCTION
+
+ can_be_run = FALSE
+ ability_path = /datum/action/innate/ai/ranged/charge_borg_or_apc
+ ability_recharge_cost = 1500
+
+/datum/ai_project/induction_apc/finish()
+ var/datum/action/innate/ai/ranged/charge_borg_or_apc/ability = add_ability(/datum/action/innate/ai/ranged/charge_borg_or_apc)
+ var/obj/effect/proc_holder/ranged_ai/charge_borg_or_apc/effect = ability.linked_ability
+ if(ability)
+ effect.works_on_apcs = TRUE
+ effect.attached_action.button.name = "Charge APC"
+ effect.attached_action.button.desc = "Click an APC to charge it by 33%"
+ else
+ ability = locate(/datum/action/innate/ai/ranged/charge_borg_or_apc) in ai.actions
+ effect = ability.linked_ability
+ effect.works_on_apcs = TRUE
+ effect.attached_action.button.name = "Charge cyborg/APC"
+ effect.attached_action.button.desc = "Click a cyborg or APC to charge it by 33%"
+
+
+/datum/action/innate/ai/ranged/charge_borg_or_apc
+ name = "Charge cyborg/APC"
+ desc = "Depending on upgrades you can charge either a single cyborg or APC in view by 33%"
+ button_icon_state = "electrified"
+ uses = 1
+ delete_on_empty = FALSE
+ linked_ability_type = /obj/effect/proc_holder/ranged_ai/charge_borg_or_apc
+
+/datum/action/innate/ai/ranged/charge_borg_or_apc/proc/charge_borg_or_apc(atom/target)
+ if(target && !QDELETED(target))
+ if(istype(target, /mob/living/silicon/robot))
+ var/mob/living/silicon/robot/R = target
+ log_game("[key_name(usr)] charged [R.name].")
+ if(R.cell)
+ if(R.cell.charge >= R.cell.maxcharge)
+ to_chat(owner, span_warning("[R]'s power cell is already full!"))
+ return FALSE
+ R.charge(null, R.cell.maxcharge * 0.33)
+ return TRUE
+ else
+ to_chat(owner, span_warning("[R] has no powercell to charge!"))
+ else if(istype(target, /obj/machinery/power/apc))
+ var/obj/machinery/power/apc/APC = target
+ var/turf/T = get_turf(APC)
+ log_game("[key_name(usr)] charged [APC.name] at [AREACOORD(T)].")
+ if(APC.cell)
+ if(APC.cell.charge >= APC.cell.maxcharge)
+ to_chat(owner, span_warning("The APC is already fully charged!"))
+ return FALSE
+ APC.cell.give(APC.cell.maxcharge * 0.33)
+ return TRUE
+ else
+ to_chat(owner, span_warning("The APC has no powercell to charge!"))
+
+/obj/effect/proc_holder/ranged_ai/charge_borg_or_apc
+ active = FALSE
+ var/works_on_borgs = FALSE
+ var/works_on_apcs = FALSE
+ enable_text = span_notice("You prepare bluespace induction coils. Click a borg or APC to charge its cell by 33%")
+ disable_text = span_notice("You power down your induction coils.")
+
+/obj/effect/proc_holder/ranged_ai/charge_borg_or_apc/InterceptClickOn(mob/living/caller, params, atom/target)
+ if(..())
+ return
+ if(ranged_ability_user.incapacitated())
+ remove_ranged_ability()
+ return
+ if(!istype(target, /mob/living/silicon/robot) && !istype(target, /obj/machinery/power/apc))
+ to_chat(ranged_ability_user, span_warning("You can only charge cyborgs or APCs!"))
+ return
+ if(!works_on_borgs && istype(target, /mob/living/silicon/robot))
+ to_chat(ranged_ability_user, span_warning("You can only charge APCs!"))
+ return
+ if(!works_on_apcs && istype(target, /obj/machinery/power/apc))
+ to_chat(ranged_ability_user, span_warning("You can only charge cyborgs!"))
+ return
+
+ ranged_ability_user.playsound_local(ranged_ability_user, "sparks", 50, 0)
+
+ var/datum/action/innate/ai/ranged/charge_borg_or_apc/action = attached_action
+ if(action.charge_borg_or_apc(target))
+ attached_action.adjust_uses(-1)
+ do_sparks(3, FALSE, target)
+ to_chat(caller, span_notice("You charge [target]."))
+ target.audible_message(span_userdanger("You hear a soothing electrical buzzing sound coming from [target]!"))
+ remove_ranged_ability()
+ return TRUE
diff --git a/code/modules/mob/living/silicon/ai/decentralized/projects/surveillance.dm b/code/modules/mob/living/silicon/ai/decentralized/projects/surveillance.dm
new file mode 100644
index 000000000000..e88ab17fec35
--- /dev/null
+++ b/code/modules/mob/living/silicon/ai/decentralized/projects/surveillance.dm
@@ -0,0 +1,36 @@
+/datum/ai_project/camera_tracker
+ name = "Camera Memory Tracker"
+ description = "Using complex LSTM nodes it is possible to automatically detect when a tagged individual enters camera visiblity."
+ research_cost = 4000
+ ram_required = 4
+ research_requirements_text = "Examination Upgrade"
+ research_requirements = list(/datum/ai_project/examine_humans)
+ category = AI_PROJECT_SURVEILLANCE
+
+/datum/ai_project/camera_tracker/run_project(force_run = FALSE)
+ . = ..(force_run)
+ if(!.)
+ return .
+ ai.canCameraMemoryTrack = TRUE
+ ai.add_verb_ai(/mob/living/silicon/ai/proc/choose_camera_target)
+
+/datum/ai_project/camera_tracker/stop()
+ ai.canCameraMemoryTrack = FALSE
+ remove_verb(ai, /mob/living/silicon/ai/proc/choose_camera_target)
+ ..()
+
+/mob/living/silicon/ai/proc/choose_camera_target()
+ set name = "Choose Camera Memory Target"
+ set category = "AI Commands"
+ set desc = "Select a target for the camera memory tracker. Case sensitive. "
+ var/target = stripped_input(usr, "Please enter target name (Leave empty for cancel):", "Camera Tracker", "", MAX_NAME_LEN)
+ if(!target)
+ cameraMemoryTarget = null
+ return
+ if(cameraMemoryTarget)
+ to_chat(usr, span_warning("Old target discarded. Exclusively tracking new target."))
+ else
+ to_chat(usr, span_notice("Now tracking new target, [target]."))
+
+ cameraMemoryTarget = target
+ cameraMemoryTickCount = 0
diff --git a/code/modules/mob/living/silicon/ai/life.dm b/code/modules/mob/living/silicon/ai/life.dm
index 19890e96c9a9..eee5887204aa 100644
--- a/code/modules/mob/living/silicon/ai/life.dm
+++ b/code/modules/mob/living/silicon/ai/life.dm
@@ -25,7 +25,7 @@
// messenging the client
malfhacked(malfhack)
- if(isturf(loc) && (QDELETED(eyeobj) || !eyeobj.loc))
+ if(isvalidAIloc(loc) && (QDELETED(eyeobj) || !eyeobj.loc))
view_core()
if(machine)
@@ -55,6 +55,25 @@
else if(!aiRestorePowerRoutine)
ai_lose_power()
+
+ if(cameraMemoryTarget)
+ if(cameraMemoryTickCount >= AI_CAMERA_MEMORY_TICKS)
+ cameraMemoryTickCount = 0
+ trackable_mobs()
+ var/list/trackeable = track.humans
+ var/list/target = list()
+ for(var/I in trackeable)
+ var/mob/M = trackeable[I]
+ if(M.name == cameraMemoryTarget)
+ target += M
+ if(name == cameraMemoryTarget)
+ target += src
+ if(target.len)
+ to_chat(src, span_notice("Tracked target [cameraMemoryTarget] found visible on cameras. Tracking disabled."))
+ cameraMemoryTarget = 0
+
+ cameraMemoryTickCount++
+
/mob/living/silicon/ai/proc/lacks_power()
var/turf/T = get_turf(src)
diff --git a/icons/mob/actions/actions_AI.dmi b/icons/mob/actions/actions_AI.dmi
index 2ddc7923cc3e..8c92c6ba09a9 100644
Binary files a/icons/mob/actions/actions_AI.dmi and b/icons/mob/actions/actions_AI.dmi differ
diff --git a/tgui/packages/tgui/interfaces/AiDashboard.js b/tgui/packages/tgui/interfaces/AiDashboard.js
index 37963f159e6b..6c79ce457ae6 100644
--- a/tgui/packages/tgui/interfaces/AiDashboard.js
+++ b/tgui/packages/tgui/interfaces/AiDashboard.js
@@ -1,5 +1,5 @@
import { Fragment } from 'inferno';
-import { useBackend, useLocalState } from '../backend';
+import { useBackend, useLocalState, useSharedState } from '../backend';
import { Box, Button, Tabs, ProgressBar, Section, Divider, LabeledControls, NumberInput } from '../components';
import { Window } from '../layouts';
@@ -7,8 +7,9 @@ export const AiDashboard = (props, context) => {
const { act, data } = useBackend(context);
- const [tab, setTab] = useLocalState(context, 'tab', 1);
- const [selectedCategory, setCategory] = useLocalState(context, 'selectedCategory', data.categories[0]);
+ const [tab, setTab] = useSharedState(context, 'tab', 1);
+ const [selectedCategory, setCategory] = useSharedState(context, 'selectedCategory', data.categories[0]);
+ const [activeProjectsOnly, setActiveProjectsOnly] = useSharedState(context, 'activeProjectsOnly', false);
return (
{
setTab(3))}>
+ Ability Charging
+
+ setTab(4))}>
Cloud Resources
@@ -131,11 +137,14 @@ export const AiDashboard = (props, context) => {
THz
)}>
- Research Cost: {project.research_cost} THz
- RAM Requirement: {project.ram_required} TB
- Research Requirements:  
- {project.research_requirements}
-
+ Research Cost:
+ {project.research_cost} THz
+
+ RAM Requirement:
+ {project.ram_required} TB
+
+ Research Requirements:
+ {project.research_requirements}
{project.description}
@@ -145,7 +154,7 @@ export const AiDashboard = (props, context) => {
)}
{tab === 2 && (
-
+ setActiveProjectsOnly(!activeProjectsOnly)}>See Runnable Projects Only)}>
{data.categories.map((category, index) => (
{
))}
{data.completed_projects.filter(project => {
+ if (activeProjectsOnly && !project.can_be_run) {
+ return false;
+ }
return project.category === selectedCategory;
}).map((project, index) => (
- {project.name} | {project.running ? "Running" : "Not Running"})} buttons={(
-
- )}>
- RAM Requirement: {project.ram_required} TB
+ {project.name} | {project.can_be_run ? project.running ? "Running" : "Not Running" : "Passive"})}
+ buttons={!!project.can_be_run && (
+
+ )}>
+ {!!project.can_be_run && (
+ RAM Requirement: {project.ram_required} TB
+ )}
{project.description}
@@ -173,6 +189,32 @@ export const AiDashboard = (props, context) => {
)}
{tab === 3 && (
+
+ {data.chargeable_abilities.filter(ability => {
+ return ability.uses < ability.max_uses;
+ }).map((ability, index) => (
+
+ {ability.name} | Uses Remaining: {ability.uses}/{ability.max_uses}
+
+ )}
+ buttons={(
+
+ Assigned CPU:
+ act('allocate_recharge_cpu', {
+ project_name: ability.project_name,
+ amount: value,
+ })} />
+ THz
+
+ )}>
+
+
+ ))}
+
+ )}
+ {tab === 4 && (
)}
-
-
);
diff --git a/yogstation.dme b/yogstation.dme
index 4bed84d3f70e..9abf2a4a34a1 100644
--- a/yogstation.dme
+++ b/yogstation.dme
@@ -2348,6 +2348,8 @@
#include "code\modules\mob\living\silicon\ai\decentralized\projects\camera_mobility.dm"
#include "code\modules\mob\living\silicon\ai\decentralized\projects\examine.dm"
#include "code\modules\mob\living\silicon\ai\decentralized\projects\firewall.dm"
+#include "code\modules\mob\living\silicon\ai\decentralized\projects\induction.dm"
+#include "code\modules\mob\living\silicon\ai\decentralized\projects\surveillance.dm"
#include "code\modules\mob\living\silicon\ai\freelook\cameranet.dm"
#include "code\modules\mob\living\silicon\ai\freelook\chunk.dm"
#include "code\modules\mob\living\silicon\ai\freelook\eye.dm"