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"